fix: miscelaneous fixes

Former-commit-id: f008552b7a5ca5f57a9802df6e7b1a09102afc74
This commit is contained in:
cschen 2024-08-29 08:17:52 +02:00
parent 296ef0ad36
commit eb18401e91
5 changed files with 260 additions and 151 deletions

View file

@ -19,6 +19,13 @@
"file": "${packages}/CodempClient/README.md" "file": "${packages}/CodempClient/README.md"
} }
}, },
{
"caption": "Codemp: Dump Internals",
"command": "codemp_client_dump",
"args": {
// "server_host": "http://[::1]:50051"
}
},
{ {
// # on_window_command, does not trigger when called from the command palette // # on_window_command, does not trigger when called from the command palette
// # See: https://github.com/sublimehq/sublime_text/issues/2234 // # See: https://github.com/sublimehq/sublime_text/issues/2234
@ -64,4 +71,20 @@
// 'buffer_id': 'test' // 'buffer_id': 'test'
} }
}, },
{
"caption": "Codemp: Create Buffer",
"command": "codemp_create_buffer",
"arg": {
// 'workspace_id': 'asd'
// 'buffer_id': 'test'
}
},
{
"caption": "Codemp: Delete Buffer",
"command": "codemp_delete_buffer",
"arg": {
// 'workspace_id': 'asd'
// 'buffer_id': 'test'
}
},
] ]

100
plugin.py
View file

@ -83,11 +83,12 @@ class EventListener(sublime_plugin.EventListener):
class CodempClientViewEventListener(sublime_plugin.ViewEventListener): class CodempClientViewEventListener(sublime_plugin.ViewEventListener):
@classmethod @classmethod
def is_applicable(cls, settings): def is_applicable(cls, settings):
logger.debug(settings.get(g.CODEMP_BUFFER_TAG, False))
return settings.get(g.CODEMP_BUFFER_TAG, False) return settings.get(g.CODEMP_BUFFER_TAG, False)
@classmethod @classmethod
def applies_to_primary_view_only(cls): def applies_to_primary_view_only(cls):
return False return True
def on_selection_modified_async(self): def on_selection_modified_async(self):
region = self.view.sel()[0] region = self.view.sel()[0]
@ -97,15 +98,22 @@ class CodempClientViewEventListener(sublime_plugin.ViewEventListener):
vws = client.workspace_from_view(self.view) vws = client.workspace_from_view(self.view)
vbuff = client.buffer_from_view(self.view) vbuff = client.buffer_from_view(self.view)
if vws is None or vbuff is None: if vws is None or vbuff is None:
raise logger.error("we couldn't find the matching buffer or workspace!")
return
logger.debug(f"selection modified! {vws.id}, {vbuff.id} - {start}, {end}")
vws.send_cursor(vbuff.id, start, end) vws.send_cursor(vbuff.id, start, end)
def on_activated(self): def on_activated(self):
global TEXT_LISTENER global TEXT_LISTENER
vbuff = client.buffer_from_view(self.view)
logging.debug(f"'{vbuff.id}' view activated!")
safe_listener_attach(TEXT_LISTENER, self.view.buffer()) # pyright: ignore safe_listener_attach(TEXT_LISTENER, self.view.buffer()) # pyright: ignore
def on_deactivated(self): def on_deactivated(self):
global TEXT_LISTENER global TEXT_LISTENER
vbuff = client.buffer_from_view(self.view)
logging.debug(f"'{vbuff.id}' view deactivated!")
safe_listener_detach(TEXT_LISTENER) # pyright: ignore safe_listener_detach(TEXT_LISTENER) # pyright: ignore
def on_pre_close(self): def on_pre_close(self):
@ -147,6 +155,7 @@ class CodempClientTextChangeListener(sublime_plugin.TextChangeListener):
vbuff = client.buffer_from_view(self.buffer.primary_view()) vbuff = client.buffer_from_view(self.buffer.primary_view())
if vbuff is not None: if vbuff is not None:
# but then we block the main one for the actual sending! # but then we block the main one for the actual sending!
logger.debug(f"local buffer change! {vbuff.id}")
sublime.set_timeout(lambda: vbuff.send_buffer_change(changes)) sublime.set_timeout(lambda: vbuff.send_buffer_change(changes))
@ -167,6 +176,14 @@ class CodempClientTextChangeListener(sublime_plugin.TextChangeListener):
# replace_text: swaps the content of a view with the given text. # replace_text: swaps the content of a view with the given text.
class CodempClientDumpCommand(sublime_plugin.WindowCommand):
def is_enabled(self) -> bool:
return super().is_enabled()
def run(self):
client.dump()
# Client Commands # Client Commands
############################################################################# #############################################################################
# Connect Command # Connect Command
@ -195,7 +212,7 @@ class CodempConnectCommand(sublime_plugin.WindowCommand):
def input(self, args): def input(self, args):
if "server_host" not in args: if "server_host" not in args:
return SimpleTextInput( return SimpleTextInput(
("server_host", "http://127.0.0.1:50051"), ("server_host", "http://codemp.alemi.dev:50053"),
("user_name", f"user-{random.random()}"), ("user_name", f"user-{random.random()}"),
) )
@ -216,23 +233,22 @@ class CodempJoinWorkspaceCommand(sublime_plugin.WindowCommand):
def run(self, workspace_id): def run(self, workspace_id):
assert client.codemp is not None assert client.codemp is not None
if client.valid_workspace(workspace_id): logger.info(f"Joining workspace: '{workspace_id}'...")
logger.info(f"Joining workspace: '{workspace_id}'...") promise = client.codemp.join_workspace(workspace_id)
promise = client.codemp.join_workspace(workspace_id) active_window = sublime.active_window()
active_window = sublime.active_window()
def defer_instantiation(promise): def defer_instantiation(promise):
try: try:
workspace = promise.wait() workspace = promise.wait()
except Exception as e: except Exception as e:
logger.error( logger.error(
f"Could not join workspace '{workspace_id}'.\n\nerror: {e}" f"Could not join workspace '{workspace_id}'.\n\nerror: {e}"
) )
sublime.error_message(f"Could not join workspace '{workspace_id}'") sublime.error_message(f"Could not join workspace '{workspace_id}'")
return return
client.install_workspace(workspace, active_window) client.install_workspace(workspace, active_window)
sublime.set_timeout_async(lambda: defer_instantiation(promise)) sublime.set_timeout_async(lambda: defer_instantiation(promise))
# the else shouldn't really happen, and if it does, it should already be instantiated. # the else shouldn't really happen, and if it does, it should already be instantiated.
# ignore. # ignore.
@ -241,7 +257,7 @@ class CodempJoinWorkspaceCommand(sublime_plugin.WindowCommand):
def input(self, args): def input(self, args):
if "workspace_id" not in args: if "workspace_id" not in args:
return SimpleTextInput(("workspace_id", "")) return SimpleTextInput(("workspace_id", "workspace?"))
# To allow for having a selection and choosing non existing workspaces # To allow for having a selection and choosing non existing workspaces
@ -284,8 +300,11 @@ class CodempLeaveWorkspaceCommand(sublime_plugin.WindowCommand):
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): def run(self, workspace_id: str):
# client.leave_workspace(id) assert client.codemp is not None
pass if client.codemp.leave_workspace(workspace_id):
vws = client.workspace_from_id(workspace_id)
if vws is not None:
client.uninstall_workspace(vws)
def input(self, args): def input(self, args):
if "id" not in args: if "id" not in args:
@ -310,8 +329,10 @@ class CodempJoinBufferCommand(sublime_plugin.WindowCommand):
# if not existing ask for creation (create + attach) # if not existing ask for creation (create + attach)
vws = client.workspace_from_id(workspace_id) vws = client.workspace_from_id(workspace_id)
assert vws is not None assert vws is not None
# is the buffer already installed? # is the buffer already installed?
if vws.valid_buffer(buffer_id): if vws.valid_buffer(buffer_id):
logger.debug("buffer already installed!")
return # do nothing. return # do nothing.
if buffer_id not in vws.codemp.filetree(filter=buffer_id): if buffer_id not in vws.codemp.filetree(filter=buffer_id):
@ -330,17 +351,22 @@ class CodempJoinBufferCommand(sublime_plugin.WindowCommand):
create_promise.wait() create_promise.wait()
# now we can defer the attaching process # now we can defer the attaching process
logger.debug(f"attempting to attach to {buffer_id}...")
promise = vws.codemp.attach(buffer_id) promise = vws.codemp.attach(buffer_id)
def deferred_attach(promise): def deferred_attach(promise):
try: try:
buff_ctl = promise.wait() buff_ctl = promise.wait()
logger.debug("attach successfull!")
except Exception as e: except Exception as e:
logging.error(f"error when attaching to buffer '{id}':\n\n {e}") logging.error(f"error when attaching to buffer '{id}':\n\n {e}")
sublime.error_message(f"Could not attach to buffer '{buffer_id}'") sublime.error_message(f"Could not attach to buffer '{buffer_id}'")
return return
vbuff = vws.install_buffer(buff_ctl) vbuff = vws.install_buffer(buff_ctl)
# TODO! if the view is already active calling focus_view() will not trigger the on_activate client.register_buffer(vws, vbuff) # we need to keep track of it.
# TODO! if the view is already active calling focus_view()
# will not trigger the on_activate
self.window.focus_view(vbuff.view) self.window.focus_view(vbuff.view)
sublime.set_timeout_async(lambda: deferred_attach(promise)) sublime.set_timeout_async(lambda: deferred_attach(promise))
@ -350,7 +376,7 @@ class CodempJoinBufferCommand(sublime_plugin.WindowCommand):
def input(self, args): def input(self, args):
if "workspace_id" not in args: if "workspace_id" not in args:
return ActiveWorkspacesIdList(self.window, get_buffer=True) return ActiveWorkspacesIdList(self.window, buffer_list=True)
if "buffer_id" not in args: if "buffer_id" not in args:
return BufferIdList(args["workspace_id"]) return BufferIdList(args["workspace_id"])
@ -373,6 +399,7 @@ class CodempLeaveBufferCommand(sublime_plugin.WindowCommand):
def defer_detach(): def defer_detach():
if vws.codemp.detach(buffer_id): if vws.codemp.detach(buffer_id):
vws.uninstall_buffer(vbuff) vws.uninstall_buffer(vbuff)
client.unregister_buffer(vbuff)
sublime.set_timeout_async(defer_detach) sublime.set_timeout_async(defer_detach)
@ -381,7 +408,7 @@ class CodempLeaveBufferCommand(sublime_plugin.WindowCommand):
def input(self, args): def input(self, args):
if "workspace_id" not in args: if "workspace_id" not in args:
return ActiveWorkspacesIdList(self.window, get_buffer=True) return ActiveWorkspacesIdList(self.window, buffer_list=True)
if "buffer_id" not in args: if "buffer_id" not in args:
return BufferIdList(args["workspace_id"]) return BufferIdList(args["workspace_id"])
@ -413,10 +440,12 @@ class CodempCreateBufferCommand(sublime_plugin.WindowCommand):
def input(self, args): def input(self, args):
if "workspace_id" not in args: if "workspace_id" not in args:
return ActiveWorkspacesIdList(self.window, get_buffer=True) return ActiveWorkspacesIdList(self.window, buffer_text=True)
if "buffer_id" not in args: if "buffer_id" not in args:
return SimpleTextInput(("buffer_id", "new buffer")) return SimpleTextInput(
(("buffer_id", "new buffer")),
)
class CodempDeleteBufferCommand(sublime_plugin.WindowCommand): class CodempDeleteBufferCommand(sublime_plugin.WindowCommand):
@ -475,7 +504,7 @@ class CodempDeleteBufferCommand(sublime_plugin.WindowCommand):
def input(self, args): def input(self, args):
if "workspace_id" not in args: if "workspace_id" not in args:
return ActiveWorkspacesIdList(self.window, get_buffer=True) return ActiveWorkspacesIdList(self.window, buffer_list=True)
if "buffer_id" not in args: if "buffer_id" not in args:
return BufferIdList(args["workspace_id"]) return BufferIdList(args["workspace_id"])
@ -494,7 +523,7 @@ class CodempReplaceTextCommand(sublime_plugin.TextCommand):
############################################################ ############################################################
class SimpleTextInput(sublime_plugin.TextInputHandler): class SimpleTextInput(sublime_plugin.TextInputHandler):
def __init__(self, *args: Tuple[str, str]): def __init__(self, *args: Tuple[str, str]):
assert len(args) > 0 logging.debug(f"why isn't the text input working? {args}")
self.argname = args[0][0] self.argname = args[0][0]
self.default = args[0][1] self.default = args[0][1]
self.next_inputs = args[1:] self.next_inputs = args[1:]
@ -506,15 +535,19 @@ class SimpleTextInput(sublime_plugin.TextInputHandler):
return self.argname return self.argname
def next_input(self, args): def next_input(self, args):
logging.debug(
f"why isn't the text input working? {self.argname}, {self.default}"
)
if len(self.next_inputs) > 0: if len(self.next_inputs) > 0:
if self.next_inputs[0][0] not in args: if self.next_inputs[0][0] not in args:
return SimpleTextInput(*self.next_inputs) return SimpleTextInput(*self.next_inputs)
class ActiveWorkspacesIdList(sublime_plugin.ListInputHandler): class ActiveWorkspacesIdList(sublime_plugin.ListInputHandler):
def __init__(self, window=None, get_buffer=False): def __init__(self, window=None, buffer_list=False, buffer_text=False):
self.window = window self.window = window
self.get_buffer = get_buffer self.buffer_list = buffer_list
self.buffer_text = buffer_text
def name(self): def name(self):
return "workspace_id" return "workspace_id"
@ -523,14 +556,17 @@ class ActiveWorkspacesIdList(sublime_plugin.ListInputHandler):
return [vws.id for vws in client.all_workspaces(self.window)] return [vws.id for vws in client.all_workspaces(self.window)]
def next_input(self, args): def next_input(self, args):
if self.get_buffer: if self.buffer_list:
return BufferIdList(args["workspace_id"]) return BufferIdList(args["workspace_id"])
elif self.buffer_text:
return SimpleTextInput(("buffer_id", "new buffer"))
class BufferIdList(sublime_plugin.ListInputHandler): class BufferIdList(sublime_plugin.ListInputHandler):
def __init__(self, workspace_id): def __init__(self, workspace_id):
vws = client.workspace_from_id(workspace_id)
self.add_entry_text = "* create new..." self.add_entry_text = "* create new..."
self.list = [vbuff.id for vbuff in client.all_buffers(workspace_id)] self.list = vws.codemp.filetree(None)
self.list.sort() self.list.sort()
self.list.append(self.add_entry_text) self.list.append(self.add_entry_text)
self.preselected = None self.preselected = None

View file

@ -1,13 +1,53 @@
from __future__ import annotations
import sublime import sublime
import os import os
import logging import logging
import codemp import codemp
from Codemp.src import globals as g from Codemp.src import globals as g
from Codemp.src.utils import populate_view
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def make_bufferchange_cb(buff: VirtualBuffer):
def __callback(bufctl: codemp.BufferController):
def get_change_and_apply():
change_id = buff.view.change_id()
while change := bufctl.try_recv().wait():
logger.debug("received remote buffer change!")
if change is None:
break
if change.is_empty():
logger.debug("change is empty. skipping.")
continue
# In case a change arrives to a background buffer, just apply it.
# We are not listening on it. Otherwise, interrupt the listening
# to avoid echoing back the change just received.
if buff.view.id() == g.ACTIVE_CODEMP_VIEW:
buff.view.settings()[g.CODEMP_IGNORE_NEXT_TEXT_CHANGE] = True
# we need to go through a sublime text command, since the method,
# view.replace needs an edit token, that is obtained only when calling
# a textcommand associated with a view.
buff.view.run_command(
"codemp_replace_text",
{
"start": change.start,
"end": change.end,
"content": change.content,
"change_id": change_id,
}, # pyright: ignore
)
sublime.set_timeout(get_change_and_apply)
return __callback
# This class is used as an abstraction between the local buffers (sublime side) and the # This class is used as an abstraction between the local buffers (sublime side) and the
# remote buffers (codemp side), to handle the syncronicity. # remote buffers (codemp side), to handle the syncronicity.
# This class is mainly manipulated by a VirtualWorkspace, that manages its buffers # This class is mainly manipulated by a VirtualWorkspace, that manages its buffers
@ -25,31 +65,21 @@ class VirtualBuffer:
def __hash__(self) -> int: def __hash__(self) -> int:
return hash(self.id) return hash(self.id)
def sync(self):
promise = self.buffctl.content()
def defer_sync(promise):
content = promise.wait()
populate_view(self.view, content)
sublime.set_timeout_async(lambda: defer_sync(promise))
def cleanup(self): def cleanup(self):
self.uninstall() self.uninstall()
self.buffctl.stop() self.buffctl.stop()
def install(self, rootdir):
if self.installed:
return
self.tmpfile = os.path.join(rootdir, self.id)
open(self.tmpfile, "a").close()
self.view.set_scratch(True)
self.view.set_name(self.id)
self.view.retarget(self.tmpfile)
s = self.view.settings()
self.view.set_status(g.SUBLIME_STATUS_ID, "[Codemp]")
s[g.CODEMP_BUFFER_TAG] = True
self.__activate()
self.installed = True
def uninstall(self): def uninstall(self):
if not self.installed: if not getattr(self, "installed", False):
return return
self.__deactivate() self.__deactivate()
@ -62,9 +92,29 @@ class VirtualBuffer:
self.installed = False self.installed = False
def install(self, rootdir):
if getattr(self, "installed", False):
return
self.tmpfile = os.path.join(rootdir, self.id)
open(self.tmpfile, "a").close()
# self.view.set_scratch(True)
self.view.set_name(self.id)
self.view.retarget(self.tmpfile)
s = self.view.settings()
self.view.set_status(g.SUBLIME_STATUS_ID, "[Codemp]")
s[g.CODEMP_BUFFER_TAG] = True
self.sync()
self.__activate()
self.installed = True
def __activate(self): def __activate(self):
logger.info(f"registering a callback for buffer: {self.id}") logger.info(f"registering a callback for buffer: {self.id}")
self.buffctl.callback(self.__apply_bufferchange_cb) self.buffctl.callback(make_bufferchange_cb(self))
self.isactive = True self.isactive = True
def __deactivate(self): def __deactivate(self):
@ -85,35 +135,3 @@ class VirtualBuffer:
# we must block and wait the send request to make sure the change went through ok # we must block and wait the send request to make sure the change went through ok
self.buffctl.send(region.begin(), region.end(), change.str).wait() self.buffctl.send(region.begin(), region.end(), change.str).wait()
def __apply_bufferchange_cb(self, bufctl: codemp.BufferController):
def get_change_and_apply():
change_id = self.view.change_id()
while change := bufctl.try_recv().wait():
if change is None:
break
if change.is_empty():
logger.debug("change is empty. skipping.")
continue
# In case a change arrives to a background buffer, just apply it.
# We are not listening on it. Otherwise, interrupt the listening
# to avoid echoing back the change just received.
if self.view.id() == g.ACTIVE_CODEMP_VIEW:
self.view.settings()[g.CODEMP_IGNORE_NEXT_TEXT_CHANGE] = True
# we need to go through a sublime text command, since the method,
# view.replace needs an edit token, that is obtained only when calling
# a textcommand associated with a view.
self.view.run_command(
"codemp_replace_text",
{
"start": change.start,
"end": change.end,
"content": change.content,
"change_id": change_id,
}, # pyright: ignore
)
sublime.set_timeout(get_change_and_apply)

View file

@ -28,59 +28,81 @@ class VirtualClient:
self.driver = codemp.init(lambda msg: logger.log(logger.level, msg), False) self.driver = codemp.init(lambda msg: logger.log(logger.level, msg), False)
# bookkeeping corner # bookkeeping corner
self.__id2buffer: dict[str, VirtualBuffer] = {} self._id2buffer: dict[str, VirtualBuffer] = {}
self.__id2workspace: dict[str, VirtualWorkspace] = {} self._id2workspace: dict[str, VirtualWorkspace] = {}
self.__view2buff: dict[sublime.View, VirtualBuffer] = {} self._view2buff: dict[sublime.View, VirtualBuffer] = {}
self.__buff2workspace: bidict[VirtualBuffer, VirtualWorkspace] = bidict() self._buff2workspace: bidict[VirtualBuffer, VirtualWorkspace] = bidict()
self.__workspace2window: bidict[VirtualWorkspace, sublime.Window] = bidict() self._workspace2window: bidict[VirtualWorkspace, sublime.Window] = bidict()
def dump(self):
logger.debug("CLIENT STATUS:")
logger.debug("WORKSPACES:")
logger.debug(f"{self._id2workspace}")
logger.debug(f"{self._workspace2window}")
logger.debug(f"{self._workspace2window.inverse}")
logger.debug(f"{self._buff2workspace}")
logger.debug(f"{self._buff2workspace.inverse}")
logger.debug("VIEWS")
logger.debug(f"{self._view2buff}")
logger.debug(f"{self._id2buffer}")
def valid_window(self, window: sublime.Window): def valid_window(self, window: sublime.Window):
return window in self.__workspace2window.inverse return window in self._workspace2window.inverse
def valid_workspace(self, workspace: VirtualWorkspace | str): def valid_workspace(self, workspace: VirtualWorkspace | str):
if isinstance(workspace, str): if isinstance(workspace, str):
return client.__id2workspace.get(workspace) is not None return client._id2workspace.get(workspace) is not None
return workspace in self.__workspace2window return workspace in self._workspace2window
def all_workspaces( def all_workspaces(
self, window: Optional[sublime.Window] = None self, window: Optional[sublime.Window] = None
) -> list[VirtualWorkspace]: ) -> list[VirtualWorkspace]:
if window is None: if window is None:
return list(self.__workspace2window.keys()) return list(self._workspace2window.keys())
else: else:
return self.__workspace2window.inverse.get(window, []) return self._workspace2window.inverse.get(window, [])
def workspace_from_view(self, view: sublime.View) -> Optional[VirtualWorkspace]: def workspace_from_view(self, view: sublime.View) -> Optional[VirtualWorkspace]:
buff = self.__view2buff.get(view, None) buff = self._view2buff.get(view, None)
return self.__buff2workspace.get(buff, None) return self._buff2workspace.get(buff, None)
def workspace_from_buffer(self, buff: VirtualBuffer) -> Optional[VirtualWorkspace]: def workspace_from_buffer(self, buff: VirtualBuffer) -> Optional[VirtualWorkspace]:
return self.__buff2workspace.get(buff) return self._buff2workspace.get(buff)
def workspace_from_id(self, id: str) -> Optional[VirtualWorkspace]: def workspace_from_id(self, id: str) -> Optional[VirtualWorkspace]:
return self.__id2workspace.get(id) return self._id2workspace.get(id)
def all_buffers( def all_buffers(
self, workspace: Optional[VirtualWorkspace | str] = None self, workspace: Optional[VirtualWorkspace | str] = None
) -> list[VirtualBuffer]: ) -> list[VirtualBuffer]:
if workspace is None: if workspace is None:
return list(self.__buff2workspace.keys()) return list(self._buff2workspace.keys())
else: else:
if isinstance(workspace, str): if isinstance(workspace, str):
workspace = client.__id2workspace[workspace] workspace = client._id2workspace[workspace]
return self.__buff2workspace.inverse.get(workspace, []) return self._buff2workspace.inverse.get(workspace, [])
def buffer_from_view(self, view: sublime.View) -> Optional[VirtualBuffer]: def buffer_from_view(self, view: sublime.View) -> Optional[VirtualBuffer]:
return self.__view2buff.get(view) return self._view2buff.get(view)
def buffer_from_id(self, id: str) -> Optional[VirtualBuffer]: def buffer_from_id(self, id: str) -> Optional[VirtualBuffer]:
return self.__id2buffer.get(id) return self._id2buffer.get(id)
def view_from_buffer(self, buff: VirtualBuffer) -> sublime.View: def view_from_buffer(self, buff: VirtualBuffer) -> sublime.View:
return buff.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 unregister_buffer(self, buffer: VirtualBuffer):
del self._buff2workspace[buffer]
del self._id2buffer[buffer.id]
del self._view2buff[buffer.view]
def disconnect(self): def disconnect(self):
if self.codemp is None: if self.codemp is None:
return return
@ -90,11 +112,11 @@ class VirtualClient:
vws.cleanup() vws.cleanup()
self.codemp.leave_workspace(vws.id) self.codemp.leave_workspace(vws.id)
self.__id2workspace.clear() self._id2workspace.clear()
self.__id2buffer.clear() self._id2buffer.clear()
self.__buff2workspace.clear() self._buff2workspace.clear()
self.__view2buff.clear() self._view2buff.clear()
self.__workspace2window.clear() self._workspace2window.clear()
self.codemp = None self.codemp = None
def connect(self, host: str, user: str, password: str): def connect(self, host: str, user: str, password: str):
@ -112,22 +134,25 @@ class VirtualClient:
# we pass the window as well so if the window changes in the mean # we pass the window as well so if the window changes in the mean
# time we have the correct one! # time we have the correct one!
vws = VirtualWorkspace(workspace, window) vws = VirtualWorkspace(workspace, window)
self.__workspace2window[vws] = window self._workspace2window[vws] = window
self.__id2workspace[vws.id] = vws self._id2workspace[vws.id] = vws
vws.install() vws.install()
return vws return vws
def uninstall_workspace(self, vws: VirtualWorkspace): def uninstall_workspace(self, vws: VirtualWorkspace):
if vws not in self.__workspace2window: if vws not in self._workspace2window:
raise raise
logger.info(f"Uninstalling workspace '{vws.id}'...") logger.info(f"Uninstalling workspace '{vws.id}'...")
vws.cleanup() vws.cleanup()
del self.__id2workspace[vws.id] del self._workspace2window[vws]
del self.__workspace2window[vws] del self._id2workspace[vws.id]
self.__buff2workspace.inverse_del(vws) buffers = self._buff2workspace.inverse[vws]
for vbuff in buffers:
self.unregister_buffer(vbuff)
# self._buff2workspace.inverse_del(vws) - if we delete all straight
# keys the last delete will remove also the empty key.
def workspaces_in_server(self): def workspaces_in_server(self):
return self.codemp.active_workspaces() if self.codemp else [] return self.codemp.active_workspaces() if self.codemp else []

View file

@ -16,6 +16,28 @@ from Codemp.src.utils import bidict
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def make_cursor_callback(workspace: VirtualWorkspace):
def __callback(ctl: codemp.CursorController):
def get_event_and_draw():
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(
"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(get_event_and_draw)
return __callback
# A virtual workspace is a bridge class that aims to translate # A virtual workspace is a bridge class that aims to translate
# events that happen to the codemp workspaces into sublime actions # events that happen to the codemp workspaces into sublime actions
class VirtualWorkspace: class VirtualWorkspace:
@ -30,8 +52,8 @@ class VirtualWorkspace:
self.codemp.fetch_users() self.codemp.fetch_users()
# mapping remote ids -> local ids # mapping remote ids -> local ids
self.__buff2view: bidict[VirtualBuffer, sublime.View] = bidict() self._buff2view: bidict[VirtualBuffer, sublime.View] = bidict()
self.__id2buff: dict[str, VirtualBuffer] = {} self._id2buff: dict[str, VirtualBuffer] = {}
# self.id_map: dict[str, int] = {} # self.id_map: dict[str, int] = {}
# self.active_buffers: dict[int, VirtualBuffer] = {} # local_id -> VBuff # self.active_buffers: dict[int, VirtualBuffer] = {} # local_id -> VBuff
@ -44,27 +66,27 @@ class VirtualWorkspace:
# if not get up to speed! # if not get up to speed!
self.codemp.fetch_buffers().wait() self.codemp.fetch_buffers().wait()
attached_buffers = self.codemp.buffer_list() attached_buffers = self.codemp.buffer_list()
all(id in self.__id2buff for id in attached_buffers) all(id in self._id2buff for id in attached_buffers)
# TODO! # TODO!
def valid_buffer(self, buff: VirtualBuffer | str): def valid_buffer(self, buff: VirtualBuffer | str):
if isinstance(buff, str): if isinstance(buff, str):
return self.buff_by_id(buff) is not None return self.buff_by_id(buff) is not None
return buff in self.__buff2view return buff in self._buff2view
def all_buffers(self) -> list[VirtualBuffer]: def all_buffers(self) -> list[VirtualBuffer]:
return list(self.__buff2view.keys()) return list(self._buff2view.keys())
def buff_by_view(self, view: sublime.View) -> Optional[VirtualBuffer]: def buff_by_view(self, view: sublime.View) -> Optional[VirtualBuffer]:
buff = self.__buff2view.inverse.get(view) buff = self._buff2view.inverse.get(view)
return buff[0] if buff is not None else None return buff[0] if buff is not None else None
def buff_by_id(self, id: str) -> Optional[VirtualBuffer]: def buff_by_id(self, id: str) -> Optional[VirtualBuffer]:
return self.__id2buff.get(id) return self._id2buff.get(id)
def all_views(self) -> list[sublime.View]: def all_views(self) -> list[sublime.View]:
return list(self.__buff2view.inverse.keys()) return list(self._buff2view.inverse.keys())
def view_by_buffer(self, buffer: VirtualBuffer) -> sublime.View: def view_by_buffer(self, buffer: VirtualBuffer) -> sublime.View:
return buffer.view return buffer.view
@ -76,14 +98,14 @@ class VirtualWorkspace:
for view in self.all_views(): for view in self.all_views():
view.close() view.close()
self.__buff2view.clear()
self.__id2buff.clear()
self.uninstall() self.uninstall()
self.curctl.stop() self.curctl.stop()
self._buff2view.clear()
self._id2buff.clear()
def uninstall(self): def uninstall(self):
if not self.installed: if not getattr(self, "installed", False):
return return
self.__deactivate() self.__deactivate()
@ -106,7 +128,7 @@ class VirtualWorkspace:
self.installed = False self.installed = False
def install(self): def install(self):
if self.installed: if getattr(self, "installed", False):
return return
# initialise the virtual filesystem # initialise the virtual filesystem
@ -127,7 +149,7 @@ class VirtualWorkspace:
self.installed = True self.installed = True
def __activate(self): def __activate(self):
self.curctl.callback(self.__move_cursor_callback) self.curctl.callback(make_cursor_callback(self))
self.isactive = True self.isactive = True
def __deactivate(self): def __deactivate(self):
@ -135,11 +157,13 @@ class VirtualWorkspace:
self.isactive = False self.isactive = False
def install_buffer(self, buff: codemp.BufferController) -> VirtualBuffer: def install_buffer(self, buff: codemp.BufferController) -> VirtualBuffer:
logger.debug(f"installing buffer {buff.name()}")
view = self.window.new_file() view = self.window.new_file()
vbuff = VirtualBuffer(buff, view) vbuff = VirtualBuffer(buff, view)
self.__buff2view[vbuff] = view logger.debug("created virtual buffer")
self.__id2buff[vbuff.id] = vbuff self._buff2view[vbuff] = view
self._id2buff[vbuff.id] = vbuff
vbuff.install(self.rootdir) vbuff.install(self.rootdir)
@ -148,28 +172,11 @@ class VirtualWorkspace:
def uninstall_buffer(self, vbuff: VirtualBuffer): def uninstall_buffer(self, vbuff: VirtualBuffer):
vbuff.cleanup() vbuff.cleanup()
buffview = self.view_by_buffer(vbuff) buffview = self.view_by_buffer(vbuff)
del self.__buff2view[vbuff] del self._buff2view[vbuff]
del self.__id2buff[vbuff.id] del self._id2buff[vbuff.id]
buffview.close() buffview.close()
def send_cursor(self, id: str, start: Tuple[int, int], end: Tuple[int, int]): 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 # we can safely ignore the promise, we don't really care if everything
# is ok for now with the cursor. # is ok for now with the cursor.
self.curctl.send(id, start, end) self.curctl.send(id, start, end)
def __move_cursor_callback(self, ctl: codemp.CursorController):
def get_event_and_draw():
while event := ctl.try_recv().wait():
if event is None:
break
vbuff = self.buff_by_id(event.buffer)
if vbuff is None:
logger.warning(
"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(get_event_and_draw)