chore: continuing refactor. should be almost done now. Cleaned up registries, commands and improved lookup logic.

This commit is contained in:
cschen 2024-11-02 18:26:37 +01:00
parent 74f386e819
commit 8565356d38
8 changed files with 388 additions and 337 deletions

73
main.py
View file

@ -73,11 +73,12 @@ class EventListener(sublime_plugin.EventListener):
# client.driver.stop() # client.driver.stop()
def on_pre_close_window(self, window): def on_pre_close_window(self, window):
assert client.codemp is not None assert session.client is not None
for vws in client.all_workspaces(window): for vws in workspaces.lookup(window):
client.codemp.leave_workspace(vws.id) sublime.run_command("codemp_leave_workspace", {
client.uninstall_workspace(vws) "workspace_id": vws.id
})
def on_text_command(self, view, command_name, args): def on_text_command(self, view, command_name, args):
if command_name == "codemp_replace_text": if command_name == "codemp_replace_text":
@ -91,7 +92,7 @@ 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):
return settings.get(g.CODEMP_BUFFER_TAG) is not None return settings.get(g.CODEMP_VIEW_TAG) is not None
@classmethod @classmethod
def applies_to_primary_view_only(cls): def applies_to_primary_view_only(cls):
@ -102,14 +103,17 @@ class CodempClientViewEventListener(sublime_plugin.ViewEventListener):
start = self.view.rowcol(region.begin()) start = self.view.rowcol(region.begin())
end = self.view.rowcol(region.end()) end = self.view.rowcol(region.end())
vws = client.workspace_from_view(self.view) try:
vbuff = client.buffer_from_view(self.view) _, vws, vbuff = objects_from_view(self.view)
if vws is None or vbuff is None: except ValueError:
logger.error("we couldn't find the matching buffer or workspace!") logger.error(f"Could not find buffers associated with the view {self.view}.\
Removig the tag to disable event listener. Reattach.")
# delete the tag so we disable this event listener on the view
del self.view.settings()[g.CODEMP_VIEW_TAG]
return 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)
logger.debug(f"selection modified! {vws.id}, {vbuff.id} - {start}, {end}")
def on_activated(self): def on_activated(self):
global TEXT_LISTENER global TEXT_LISTENER
@ -126,16 +130,12 @@ class CodempClientViewEventListener(sublime_plugin.ViewEventListener):
logger.debug("closing active view") logger.debug("closing active view")
global TEXT_LISTENER global TEXT_LISTENER
safe_listener_detach(TEXT_LISTENER) # pyright: ignore safe_listener_detach(TEXT_LISTENER) # pyright: ignore
try:
vws = client.workspace_from_view(self.view) _, vws, vbuff = objects_from_view(self.view)
vbuff = client.buffer_from_view(self.view) buffers.remove(vbuff)
if vws is None or vbuff is None: except ValueError:
logger.debug("no matching workspace or buffer.")
return return
client.unregister_buffer(vbuff)
vws.uninstall_buffer(vbuff)
def on_text_command(self, command_name, args): def on_text_command(self, command_name, args):
if command_name == "codemp_replace_text": if command_name == "codemp_replace_text":
logger.info("got a codemp_replace_text command! but in the view listener") logger.info("got a codemp_replace_text command! but in the view listener")
@ -145,30 +145,16 @@ class CodempClientViewEventListener(sublime_plugin.ViewEventListener):
logger.info("got a codemp_replace_text command! but in the view listener") logger.info("got a codemp_replace_text command! but in the view listener")
class CodempClientTextChangeListener(sublime_plugin.TextChangeListener): # Next TODO:
@classmethod # Server configurations:
def is_applicable(cls, buffer): # pyright: ignore # - where do we store it?
# don't attach this event listener automatically # - TOML? yes probably toml
# we'll do it by hand with .attach(buffer).
return False
def on_text_changed(self, changes):
s = self.buffer.primary_view().settings()
if s.get(g.CODEMP_IGNORE_NEXT_TEXT_CHANGE, False):
logger.debug("Ignoring echoing back the change.")
s[g.CODEMP_IGNORE_NEXT_TEXT_CHANGE] = False
return
vbuff = client.buffer_from_view(self.buffer.primary_view())
if vbuff is not None:
logger.debug(f"local buffer change! {vbuff.id}")
vbuff.send_buffer_change(changes)
TEXT_LISTENER = CodempClientTextChangeListener()
# * Quickpanel for connecting with stuff.
# * Quickpanel for browsing the servers
# * Move all "server actions" like, create, delete, rename etc. as quickpanel actions. (See SFTP plugin.)
# * make panel for notifications!
# * make panel for errors and logging!
# Proxy Commands ( NOT USED, left just in case we need it again. ) # Proxy Commands ( NOT USED, left just in case we need it again. )
############################################################################# #############################################################################
@ -184,3 +170,8 @@ TEXT_LISTENER = CodempClientTextChangeListener()
# #
# def input_description(self): # def input_description(self):
# return 'Share Buffer:' # return 'Share Buffer:'

View file

@ -6,32 +6,13 @@ import random
import codemp import codemp
from ..core.session import session from ..core.session import session
from ..core.workspace import workspaces from ..core.workspace import workspaces
from ..core.buffers import buffers
from input_handlers import SimpleTextInput from ..input_handlers import SimpleTextInput
from input_handlers import SimpleListInput from ..input_handlers import SimpleListInput
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class CodempConnectCommand(sublime_plugin.WindowCommand): class CodempConnectCommand(sublime_plugin.WindowCommand):
def run(self, server_host, user_name, password): # pyright: ignore[reportIncompatibleMethodOverride]
def _():
try:
config = codemp.Config(
username = user_name,
password = password,
host = server_host)
session.connect(config)
except Exception as e:
sublime.error_message(
"Could not connect:\n Make sure the server is up\n\
and your credentials are correct."
)
sublime.set_timeout_async(_)
def input_description(self):
return "Server host:"
def input(self, args): def input(self, args):
if "server_host" not in args: if "server_host" not in args:
return SimpleTextInput( return SimpleTextInput(
@ -51,6 +32,26 @@ class CodempConnectCommand(sublime_plugin.WindowCommand):
("password", "password?"), ("password", "password?"),
) )
def input_description(self):
return "Server host:"
def run(self, server_host, user_name, password): # pyright: ignore[reportIncompatibleMethodOverride]
def _():
try:
config = codemp.Config(
username = user_name,
password = password,
host = server_host,
port=50053,
tls=False)
session.connect(config)
except Exception as e:
logger.error(e)
sublime.error_message(
"Could not connect:\n Make sure the server is up\n\
and your credentials are correct."
)
sublime.set_timeout_async(_)
# Disconnect Command # Disconnect Command
class CodempDisconnectCommand(sublime_plugin.WindowCommand): class CodempDisconnectCommand(sublime_plugin.WindowCommand):
@ -59,13 +60,13 @@ class CodempDisconnectCommand(sublime_plugin.WindowCommand):
def run(self): def run(self):
cli = session.client cli = session.client
assert cli is not None
for ws in workspaces.lookup(): for ws in workspaces.lookup():
if cli.leave_workspace(ws.id): if cli.leave_workspace(ws.id):
workspaces.remove(ws) workspaces.remove(ws)
session.drop_client() session.drop_client()
logger.info(f"disconnected from server '{session.config.host}'!")
# Join Workspace Command # Join Workspace Command
@ -73,117 +74,115 @@ class CodempJoinWorkspaceCommand(sublime_plugin.WindowCommand):
def is_enabled(self) -> bool: def is_enabled(self) -> bool:
return session.is_active() return session.is_active()
def run(self, workspace_id): # pyright: ignore[reportIncompatibleMethodOverride]
if workspace_id is None:
return
logger.info(f"Joining workspace: '{workspace_id}'...")
promise = session.client.join_workspace(workspace_id)
active_window = sublime.active_window()
def _():
try:
workspace = promise.wait()
except Exception as e:
logger.error(
f"Could not join workspace '{workspace_id}'.\n\nerror: {e}"
)
sublime.error_message(f"Could not join workspace '{workspace_id}'")
return
workspaces.add(workspace)
sublime.set_timeout_async(_)
def input_description(self): def input_description(self):
return "Join:" return "Join:"
def input(self, args): def input(self, args):
if "workspace_id" not in args: if "workspace_id" not in args:
list = session.client.list_workspaces(True, True) wslist = session.get_workspaces()
return SimpleListInput( return SimpleListInput(
("workspace_id", list.wait()), ("workspace_id", wslist),
) )
def run(self, workspace_id): # pyright: ignore[reportIncompatibleMethodOverride]
if workspace_id is None:
return
logger.info(f"Joining workspace: '{workspace_id}'...")
try:
ws = session.client.attach_workspace(workspace_id).wait()
except Exception as e:
logger.error(f"Could not join workspace '{workspace_id}': {e}")
sublime.error_message(f"Could not join workspace '{workspace_id}'")
return
logger.debug("Joined! Adding workspace to registry")
workspaces.add(ws)
# Leave Workspace Command # Leave Workspace Command
class CodempLeaveWorkspaceCommand(sublime_plugin.WindowCommand): class CodempLeaveWorkspaceCommand(sublime_plugin.WindowCommand):
def is_enabled(self): def is_enabled(self):
return client.codemp is not None and \ return session.is_active() and \
len(client.all_workspaces(self.window)) > 0 len(workspaces.lookup(self.window)) > 0
def run(self, workspace_id: str): # pyright: ignore[reportIncompatibleMethodOverride]
assert client.codemp is not None
if client.codemp.leave_workspace(workspace_id):
vws = client.workspace_from_id(workspace_id)
if vws is not None:
client.uninstall_workspace(vws)
else:
logger.error(f"could not leave the workspace '{workspace_id}'")
def input(self, args): def input(self, args):
if "workspace_id" not in args: if "workspace_id" not in args:
return ActiveWorkspacesIdList() wslist = session.client.active_workspaces()
return SimpleListInput(
("workspace_id", wslist),
)
def run(self, workspace_id: str): # pyright: ignore[reportIncompatibleMethodOverride]
try:
workspaces.remove(workspace_id)
finally:
if not session.client.leave_workspace(workspace_id):
logger.error(f"could not leave the workspace '{workspace_id}'")
class CodempInviteToWorkspaceCommand(sublime_plugin.WindowCommand): class CodempInviteToWorkspaceCommand(sublime_plugin.WindowCommand):
def is_enabled(self) -> bool: def is_enabled(self) -> bool:
return client.codemp is not None and len(client.all_workspaces(self.window)) > 0 return session.is_active() and len(workspaces.lookup(self.window)) > 0
def run(self, workspace_id: str, user: str): # pyright: ignore[reportIncompatibleMethodOverride]
assert client.codemp is not None
client.codemp.invite_to_workspace(workspace_id, user)
logger.debug(f"invite sent to user {user} for workspace {workspace_id}.")
def input(self, args): def input(self, args):
assert client.codemp is not None
if "workspace_id" not in args: if "workspace_id" not in args:
wslist = client.codemp.list_workspaces(True, False) wslist = session.get_workspaces(owned=True, invited=False)
return SimpleListInput( return SimpleListInput(
("workspace_id", wslist.wait()), ("user", "invitee's username") ("workspace_id", wslist), ("user", "invitee's username")
) )
if "user" not in args: if "user" not in args:
return SimpleTextInput(("user", "invitee's username")) return SimpleTextInput(("user", "invitee's username"))
def run(self, workspace_id: str, user: str): # pyright: ignore[reportIncompatibleMethodOverride]
try:
session.client.invite_to_workspace(workspace_id, user)
logger.debug(f"invite sent to user {user} for workspace {workspace_id}.")
except Exception as e:
logger.error(f"Could not invite to workspace: {e}")
class CodempCreateWorkspaceCommand(sublime_plugin.WindowCommand): class CodempCreateWorkspaceCommand(sublime_plugin.WindowCommand):
def is_enabled(self): def is_enabled(self):
return client.codemp is not None return session.is_active()
def run(self, workspace_id: str): # pyright: ignore[reportIncompatibleMethodOverride]
assert client.codemp is not None
client.codemp.create_workspace(workspace_id)
def input(self, args): def input(self, args):
if "workspace_id" not in args: if "workspace_id" not in args:
return SimpleTextInput(("workspace_id", "new workspace")) return SimpleTextInput(("workspace_id", "new workspace name"))
def run(self, workspace_id: str): # pyright: ignore[reportIncompatibleMethodOverride]
try:
session.client.create_workspace(workspace_id)
except Exception as e:
logger.error(f"Could not create workspace: {e}")
class CodempDeleteWorkspaceCommand(sublime_plugin.WindowCommand): class CodempDeleteWorkspaceCommand(sublime_plugin.WindowCommand):
def is_enabled(self): def is_enabled(self):
return client.codemp is not None return session.is_active()
def input(self, args):
workspaces = session.get_workspaces(owned=True, invited=False) # noqa: F841
if "workspace_id" not in args:
return SimpleListInput(("workspace_id", workspaces)
def run(self, workspace_id: str): # pyright: ignore[reportIncompatibleMethodOverride] def run(self, workspace_id: str): # pyright: ignore[reportIncompatibleMethodOverride]
assert client.codemp is not None try:
vws = workspaces.lookupId(workspace_id)
vws = client.workspace_from_id(workspace_id)
if vws is not None:
if not sublime.ok_cancel_dialog( if not sublime.ok_cancel_dialog(
"You are currently attached to '{workspace_id}'.\n\ "You are currently attached to '{workspace_id}'.\n\
Do you want to detach and delete it?", Do you want to detach and delete it?",
ok_title="yes", ok_title="yes", title="Delete Workspace?",
title="Delete Workspace?",
): ):
return return
if not client.codemp.leave_workspace(workspace_id): self.window.run_command(
logger.debug("error while leaving the workspace:") "codemp_leave_workspace",
raise RuntimeError("error while leaving the workspace") {"workspace_id": workspace_id})
client.uninstall_workspace(vws) except KeyError: pass
finally:
session.client.delete_workspace(workspace_id)
client.codemp.delete_workspace(workspace_id)
def input(self, args):
assert client.codemp is not None
workspaces = client.codemp.list_workspaces(True, False) # noqa: F841
if "workspace_id" not in args:
return SimpleListInput(("workspace_id", workspaces.wait()))

View file

@ -2,54 +2,74 @@ import sublime
import sublime_plugin import sublime_plugin
import logging import logging
from .src.client import client from ..core.session import session
from listeners import TEXT_LISTENER from ..core.workspace import workspaces
from input_handlers import SimpleTextInput from ..core.buffers import buffers
from input_handlers import ActiveWorkspacesIdList
from input_handlers import BufferIdList from ..text_listener import TEXT_LISTENER
from ..utils import safe_listener_attach, safe_listener_detach
from ..input_handlers import SimpleListInput, SimpleTextInput
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Join Buffer Command # Join Buffer Command
class CodempJoinBufferCommand(sublime_plugin.WindowCommand): class CodempJoinBufferCommand(sublime_plugin.WindowCommand):
def is_enabled(self): def is_enabled(self):
available_workspaces = client.all_workspaces(self.window) return len(workspaces.lookup(self.window)) > 0
return len(available_workspaces) > 0
def input_description(self) -> str:
return "Attach: "
def input(self, args):
if "workspace_id" not in args:
wslist = session.get_workspaces(owned=True, invited=True)
return SimpleListInput(
("workspace_id", wslist),
)
if "buffer_id" not in args:
try: ws = workspaces.lookupId(args["workspace_id"])
except KeyError:
sublime.error_message("Workspace does not exists or is not active.")
return None
bflist = ws.handle.fetch_buffers().wait()
return SimpleListInput(
("buffer_id", bflist),
)
def run(self, workspace_id, buffer_id): # pyright: ignore[reportIncompatibleMethodOverride] def run(self, workspace_id, buffer_id): # pyright: ignore[reportIncompatibleMethodOverride]
# A workspace has some Buffers inside of it (filetree) try: vws = workspaces.lookupId(workspace_id)
# some of those you are already attached to except KeyError:
# If already attached to it return the same alredy existing bufferctl logger.error(f"Can't create buffer: '{workspace_id}' does not exists or is not active.")
# if existing but not attached (attach) return
# if not existing ask for creation (create + attach)
vws = client.workspace_from_id(workspace_id)
assert vws is not None
# is the buffer already installed? try: # if it exists already, focus and listen
if buffer_id in vws.codemp.buffer_list(): buff = buffers.lookupId(buffer_id)
logger.info("buffer already installed!") safe_listener_detach(TEXT_LISTENER)
return # do nothing. safe_listener_attach(TEXT_LISTENER, buff.view.buffer())
self.window.focus_view(buff.view)
return
except KeyError:
pass
if buffer_id not in vws.codemp.filetree(filter=buffer_id): # if doesn't exist in the workspace, ask for creation.
create = sublime.ok_cancel_dialog( if vws.handle.get_buffer(buffer_id) is None:
"There is no buffer named '{buffer_id}' in the workspace '{workspace_id}'.\n\ if sublime.ok_cancel_dialog(
f"There is no buffer named '{buffer_id}' in the workspace '{workspace_id}'.\n\
Do you want to create it?", Do you want to create it?",
ok_title="yes", ok_title="yes", title="Create Buffer?",
title="Create Buffer?", ):
) sublime.run_command("codemp_create_buffer", {
if create: "workspace_id": workspace_id,
try: "buffer_id": buffer_id
create_promise = vws.codemp.create(buffer_id) })
except Exception as e:
logging.error(f"could not create buffer:\n\n {e}")
return
create_promise.wait()
# now we can defer the attaching process # now we can defer the attaching process
logger.debug(f"attempting to attach to {buffer_id}...") logger.debug(f"attempting to attach to {buffer_id}...")
promise = vws.codemp.attach(buffer_id) promise = vws.handle.attach_buffer(buffer_id)
def deferred_attach(promise): def _():
try: try:
buff_ctl = promise.wait() buff_ctl = promise.wait()
logger.debug("attach successfull!") logger.debug("attach successfull!")
@ -58,151 +78,144 @@ class CodempJoinBufferCommand(sublime_plugin.WindowCommand):
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, TEXT_LISTENER) safe_listener_detach(TEXT_LISTENER)
client.register_buffer(vws, vbuff) # we need to keep track of it. vbuff = buffers.add(buff_ctl, vws)
# TODO! if the view is already active calling focus_view() if self.window.active_view() == vbuff.view:
# will not trigger the on_activate # if view is already active focusing it won't trigger `on_activate`.
self.window.focus_view(vbuff.view) safe_listener_attach(TEXT_LISTENER, vbuff.view.buffer())
else:
sublime.set_timeout_async(lambda: deferred_attach(promise)) self.window.focus_view(vbuff.view)
sublime.set_timeout_async(_)
def input_description(self) -> str:
return "Attach: "
def input(self, args):
if "workspace_id" not in args:
return ActiveWorkspacesIdList(self.window, buffer_list=True)
if "buffer_id" not in args:
return BufferIdList(args["workspace_id"])
# Leave Buffer Comand # Leave Buffer Comand
class CodempLeaveBufferCommand(sublime_plugin.WindowCommand): class CodempLeaveBufferCommand(sublime_plugin.WindowCommand):
def is_enabled(self): def is_enabled(self):
return len(client.all_buffers()) > 0 return len(buffers.lookup()) > 0
def run(self, workspace_id, buffer_id): # pyright: ignore[reportIncompatibleMethodOverride]
vbuff = client.buffer_from_id(buffer_id)
vws = client.workspace_from_id(workspace_id)
if vbuff is None or vws is None:
sublime.error_message(f"You are not attached to the buffer '{id}'")
logging.warning(f"You are not attached to the buffer '{id}'")
return
def defer_detach():
if vws.codemp.detach(buffer_id):
vws.uninstall_buffer(vbuff)
client.unregister_buffer(vbuff)
sublime.set_timeout_async(defer_detach)
def input_description(self) -> str: def input_description(self) -> str:
return "Leave: " return "Leave: "
def input(self, args): def input(self, args):
if "workspace_id" not in args: if "workspace_id" not in args:
return ActiveWorkspacesIdList(self.window, buffer_list=True) wslist = session.client.active_workspaces()
return SimpleListInput(
("workspace_id", wslist),
)
if "buffer_id" not in args: if "buffer_id" not in args:
return BufferIdList(args["workspace_id"]) bflist = [bf.id for bf in buffers.lookup(args["workspace_id"])]
return SimpleListInput(
("buffer_id", bflist)
)
def run(self, workspace_id, buffer_id): # pyright: ignore[reportIncompatibleMethodOverride]
try:
buffers.lookupId(buffer_id)
vws = workspaces.lookupId(workspace_id)
except KeyError:
sublime.error_message(f"You are not attached to the buffer '{id}'")
logging.warning(f"You are not attached to the buffer '{id}'")
return
if not vws.handle.get_buffer(buffer_id):
logging.error("The desired buffer is not managed by the workspace.")
return
def _():
try:
buffers.remove(buffer_id)
finally:
if not vws.handle.detach_buffer(buffer_id):
logger.error(f"could not leave the buffer {buffer_id}.")
sublime.set_timeout_async(_)
# Leave Buffer Comand # Leave Buffer Comand
class CodempCreateBufferCommand(sublime_plugin.WindowCommand): class CodempCreateBufferCommand(sublime_plugin.WindowCommand):
def is_enabled(self): def is_enabled(self):
return len(client.all_workspaces(self.window)) > 0 return len(workspaces.lookup()) > 0
def run(self, workspace_id, buffer_id):# pyright: ignore[reportIncompatibleMethodOverride]
vws = client.workspace_from_id(workspace_id)
if vws is None:
sublime.error_message(
f"You are not attached to the workspace '{workspace_id}'"
)
logging.warning(f"You are not attached to the workspace '{workspace_id}'")
return
vws.codemp.create(buffer_id)
logging.info(
"created buffer '{buffer_id}' in the workspace '{workspace_id}'.\n\
To interact with it you need to attach to it with Codemp: Attach."
)
def input_description(self) -> str: def input_description(self) -> str:
return "Create Buffer: " return "Create Buffer: "
def input(self, args): def input(self, args):
if "workspace_id" not in args: if "workspace_id" not in args:
return ActiveWorkspacesIdList(self.window, buffer_text=True) wslist = session.client.active_workspaces()
return SimpleListInput(
("workspace_id", wslist),
)
if "buffer_id" not in args: if "buffer_id" not in args:
return SimpleTextInput( return SimpleTextInput(
(("buffer_id", "new buffer")), ("buffer_id", "new buffer name"),
) )
class CodempDeleteBufferCommand(sublime_plugin.WindowCommand):
def is_enabled(self):
return client.codemp is not None and len(client.codemp.active_workspaces()) > 0
def run(self, workspace_id, buffer_id):# pyright: ignore[reportIncompatibleMethodOverride] def run(self, workspace_id, buffer_id):# pyright: ignore[reportIncompatibleMethodOverride]
vws = client.workspace_from_id(workspace_id) try: vws = workspaces.lookupId(workspace_id)
if vws is None: except KeyError:
sublime.error_message( sublime.error_message(
f"You are not attached to the workspace '{workspace_id}'" f"You are not attached to the workspace '{workspace_id}'"
) )
logging.warning(f"You are not attached to the workspace '{workspace_id}'") logging.warning(f"You are not attached to the workspace '{workspace_id}'")
return return
fetch_promise = vws.codemp.fetch_buffers() vws.handle.create_buffer(buffer_id)
delete = sublime.ok_cancel_dialog( logging.info(
f"Confirm you want to delete the buffer '{buffer_id}'", "created buffer '{buffer_id}' in the workspace '{workspace_id}'.\n\
ok_title="delete", To interact with it you need to attach to it with Codemp: Attach."
title="Delete Buffer?",
) )
if not delete:
return
fetch_promise.wait()
existing = vws.codemp.filetree(buffer_id)
if len(existing) == 0:
sublime.error_message(
f"The buffer '{buffer_id}' does not exists in the workspace."
)
logging.info(f"The buffer '{buffer_id}' does not exists in the workspace.")
return
def deferred_delete(): class CodempDeleteBufferCommand(sublime_plugin.WindowCommand):
try: def is_enabled(self):
vws.codemp.delete(buffer_id).wait() return len(workspaces.lookup()) > 0
except Exception as e:
logging.error(
f"error when deleting the buffer '{buffer_id}':\n\n {e}", True
)
return
vbuff = client.buffer_from_id(buffer_id)
if vbuff is None:
# we are not attached to it!
sublime.set_timeout_async(deferred_delete)
else:
if vws.codemp.detach(buffer_id):
vws.uninstall_buffer(vbuff)
sublime.set_timeout_async(deferred_delete)
else:
logging.error(
f"error while detaching from buffer '{buffer_id}', aborting the delete."
)
return
def input_description(self) -> str: def input_description(self) -> str:
return "Delete buffer: " return "Delete buffer: "
def input(self, args): def input(self, args):
if "workspace_id" not in args: if "workspace_id" not in args:
return ActiveWorkspacesIdList(self.window, buffer_list=True) wslist = session.get_workspaces(owned=True, invited=False)
return SimpleListInput(
("workspace_id", wslist),
)
if "buffer_id" not in args: if "buffer_id" not in args:
return BufferIdList(args["workspace_id"]) try: ws = workspaces.lookupId(args["workspace_id"])
except KeyError:
sublime.error_message("Workspace does not exists or is not attached.")
return sublime_plugin.BackInputHandler()
bflist = ws.handle.fetch_buffers().wait()
return SimpleListInput(
("buffer_id", bflist),
)
def run(self, workspace_id, buffer_id):# pyright: ignore[reportIncompatibleMethodOverride]
try: vws = workspaces.lookupId(workspace_id)
except KeyError:
sublime.error_message(
f"You are not attached to the workspace '{workspace_id}'"
)
logging.warning(f"You are not attached to the workspace '{workspace_id}'")
return
if not sublime.ok_cancel_dialog(
f"Confirm you want to delete the buffer '{buffer_id}'",
ok_title="delete", title="Delete Buffer?",
): return
try:
buffers.lookupId(buffer_id)
if not sublime.ok_cancel_dialog(
"You are currently attached to '{buffer_id}'.\n\
Do you want to detach and delete it?",
ok_title="yes", title="Delete Buffer?",
):
return
self.window.run_command(
"codemp_leave_buffer",
{ "workspace_id": workspace_id, "buffer_id": buffer_id })
except KeyError: pass
finally:
vws.handle.delete_buffer(buffer_id).wait()

View file

@ -8,6 +8,7 @@ import sublime
import os import os
import logging import logging
from codemp import TextChange
from .. import globals as g from .. import globals as g
from ..utils import populate_view from ..utils import populate_view
from ..utils import safe_listener_attach from ..utils import safe_listener_attach
@ -20,33 +21,40 @@ def bind_callback(v: sublime.View):
def _callback(bufctl: codemp.BufferController): def _callback(bufctl: codemp.BufferController):
def _(): def _():
change_id = v.change_id() change_id = v.change_id()
while change := bufctl.try_recv().wait(): while buffup := bufctl.try_recv().wait():
logger.debug("received remote buffer change!") logger.debug("received remote buffer change!")
if change is None: if buffup is None:
break break
if change.is_empty(): if buffup.change.is_empty():
logger.debug("change is empty. skipping.") logger.debug("change is empty. skipping.")
continue continue
# In case a change arrives to a background buffer, just apply it. # In case a change arrives to a background buffer, just apply it.
# We are not listening on it. Otherwise, interrupt the listening # We are not listening on it. Otherwise, interrupt the listening
# to avoid echoing back the change just received. # to avoid echoing back the change just received.
if v.id() == g.ACTIVE_CODEMP_VIEW: if v == sublime.active_window().active_view():
v.settings()[g.CODEMP_IGNORE_NEXT_TEXT_CHANGE] = True v.settings()[g.CODEMP_IGNORE_NEXT_TEXT_CHANGE] = True
# we need to go through a sublime text command, since the method, # we need to go through a sublime text command, since the method,
# view.replace needs an edit token, that is obtained only when calling # view.replace needs an edit token, that is obtained only when calling
# a textcommand associated with a view. # a textcommand associated with a view.
v.run_command( try:
"codemp_replace_text", change = buffup.change
{ v.run_command(
"start": change.start, "codemp_replace_text",
"end": change.end, {
"content": change.content, "start": change.start,
"change_id": change_id, "end": change.end,
}, # pyright: ignore "content": change.content,
) "change_id": change_id,
}, # pyright: ignore
)
except Exception as e:
raise e
bufctl.ack(buffup.version)
sublime.set_timeout(_) sublime.set_timeout(_)
return _callback return _callback
@ -61,7 +69,6 @@ class BufferManager():
def __del__(self): def __del__(self):
logger.debug(f"dropping buffer {self.id}") logger.debug(f"dropping buffer {self.id}")
self.handle.clear_callback() self.handle.clear_callback()
self.handle.stop()
def __hash__(self): def __hash__(self):
return hash(self.id) return hash(self.id)
@ -76,8 +83,9 @@ class BufferManager():
region.begin(), region.end(), change.str region.begin(), region.end(), change.str
) )
) )
# 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.handle.send(region.begin(), region.end(), change.str).wait() self.handle.send(TextChange(start=region.begin(), end=region.end(), content=change.str))
def sync(self, text_listener): def sync(self, text_listener):
promise = self.handle.content() promise = self.handle.content()
@ -95,32 +103,43 @@ class BufferRegistry():
def lookup(self, ws: Optional[WorkspaceManager] = None) -> list[BufferManager]: def lookup(self, ws: Optional[WorkspaceManager] = None) -> list[BufferManager]:
if not ws: if not ws:
return list(self._buffers.keys()) return list(self._buffers.keys())
bf = self._buffers.inverse.get(ws) bfs = self._buffers.inverse.get(ws)
return bf if bf else [] return bfs if bfs else []
def lookupId(self, bid: str) -> Optional[BufferManager]: def lookupParent(self, bf: BufferManager | str) -> WorkspaceManager:
return next((bf for bf in self._buffers if bf.id == bid), None) if isinstance(bf, str):
bf = self.lookupId(bf)
return self._buffers[bf]
def lookupId(self, bid: str) -> BufferManager:
bfm = next((bf for bf in self._buffers if bf.id == bid), None)
if not bfm: raise KeyError
return bfm
def add(self, bhandle: codemp.BufferController, wsm: WorkspaceManager): def add(self, bhandle: codemp.BufferController, wsm: WorkspaceManager):
bid = bhandle.path() bid = bhandle.path()
tmpfile = os.path.join(wsm.rootdir, bid) # tmpfile = os.path.join(wsm.rootdir, bid)
open(tmpfile, "a").close() # open(tmpfile, "a").close()
content = bhandle.content()
win = sublime.active_window() win = sublime.active_window()
view = win.open_file(bid) view = win.open_file(bid)
view.set_scratch(True) view.set_scratch(True)
view.retarget(tmpfile) # view.retarget(tmpfile)
view.settings().set(g.CODEMP_VIEW_TAG, True) view.settings().set(g.CODEMP_VIEW_TAG, True)
view.settings().set(g.CODEMP_BUFFER_ID, bid) view.settings().set(g.CODEMP_BUFFER_ID, bid)
view.set_status(g.SUBLIME_STATUS_ID, "[Codemp]") view.set_status(g.SUBLIME_STATUS_ID, "[Codemp]")
populate_view(view, content.wait())
tmpfile = "DISABLE"
bfm = BufferManager(bhandle, view, tmpfile) bfm = BufferManager(bhandle, view, tmpfile)
self._buffers[bfm] = wsm self._buffers[bfm] = wsm
def remove(self, bf: Optional[BufferManager | str]): return bfm
def remove(self, bf: BufferManager | str):
if isinstance(bf, str): if isinstance(bf, str):
bf = self.lookupId(bf) bf = self.lookupId(bf)
if not bf: return
del self._buffers[bf] del self._buffers[bf]
bf.view.close() bf.view.close()

View file

@ -1,6 +1,8 @@
import logging import logging
import codemp import codemp
from ..utils import some
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class SessionManager(): class SessionManager():
@ -17,7 +19,7 @@ class SessionManager():
@property @property
def client(self): def client(self):
return self._client return some(self._client)
def get_or_init(self) -> codemp.Driver: def get_or_init(self) -> codemp.Driver:
if self._driver: if self._driver:
@ -48,9 +50,16 @@ class SessionManager():
self.get_or_init() self.get_or_init()
self._client = codemp.connect(config).wait() self._client = codemp.connect(config).wait()
logger.debug(f"Connected to '{config.host}' as user {self._client.user_name} (id: {self._client.user_id})") self.config = config
logger.debug(f"Connected to '{self.config.host}' as user {self._client.current_user().name} (id: {self._client.current_user().id})")
return self._client return self._client
def get_workspaces(self, owned: bool = True, invited: bool = True):
owned_wss = self.client.fetch_owned_workspaces().wait() if owned else []
invited_wss = self.client.fetch_joined_workspaces().wait() if invited else []
return owned_wss + invited_wss
def drop_client(self): def drop_client(self):
self._client = None self._client = None

View file

@ -2,14 +2,15 @@ from __future__ import annotations
from typing import Optional, Tuple from typing import Optional, Tuple
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from ...main import CodempClientTextChangeListener
import codemp import codemp
import sublime import sublime
import shutil import shutil
import tempfile import tempfile
import logging import logging
import gc
from codemp import Selection
from .. import globals as g from .. import globals as g
from ..utils import draw_cursor_region from ..utils import draw_cursor_region
from ..utils import bidict from ..utils import bidict
@ -18,9 +19,9 @@ from .buffers import buffers
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def add_project_folder(w: sublime.Window, folder: str, name: str = ""): def add_project_folder(w: sublime.Window, folder: str, name: str = ""):
proj: dict = w.project_data() # pyright: ignore proj = w.project_data()
if proj is None: if not isinstance(proj, dict):
proj = {"folders": []} # pyright: ignore, `Value` can be None proj = {"folders": []}
if name == "": if name == "":
entry = {"path": folder} entry = {"path": folder}
@ -32,7 +33,7 @@ def add_project_folder(w: sublime.Window, folder: str, name: str = ""):
w.set_project_data(proj) w.set_project_data(proj)
def remove_project_folder(w: sublime.Window, filterstr: str): def remove_project_folder(w: sublime.Window, filterstr: str):
proj: dict = self.window.project_data() # type:ignore proj: dict = w.project_data() # type:ignore
if proj is None: if proj is None:
return return
@ -51,10 +52,13 @@ def cursor_callback(ctl: codemp.CursorController):
while event := ctl.try_recv().wait(): while event := ctl.try_recv().wait():
if event is None: break if event is None: break
bfm = buffers.lookupId(event.buffer) try: bfm = buffers.lookupId(event.sel.buffer)
if not bfm: continue except KeyError: continue
draw_cursor_region(bfm.view, event.start, event.end, event.user) region_start = (event.sel.start_row, event.sel.start_col)
region_end = (event.sel.end_row, event.sel.end_col)
draw_cursor_region(bfm.view, region_start, region_end, event.user)
sublime.set_timeout_async(_) sublime.set_timeout_async(_)
class WorkspaceManager(): class WorkspaceManager():
@ -70,8 +74,8 @@ class WorkspaceManager():
logger.debug(f"dropping workspace {self.id}") logger.debug(f"dropping workspace {self.id}")
self.curctl.clear_callback() self.curctl.clear_callback()
for buff in self.handle.buffer_list(): for buff in self.handle.active_buffers():
if not self.handle.detach(buff): if not self.handle.detach_buffer(buff):
logger.warning( logger.warning(
f"could not detach from '{buff}' for workspace '{self.id}'." f"could not detach from '{buff}' for workspace '{self.id}'."
) )
@ -82,7 +86,14 @@ class WorkspaceManager():
def send_cursor(self, id: str, start: Tuple[int, int], end: Tuple[int, int]): 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) sel = Selection(
start_row=start[0],
start_col=start[1],
end_row=end[0],
end_col=end[1],
buffer=id
)
self.curctl.send(sel)
class WorkspaceRegistry(): class WorkspaceRegistry():
def __init__(self) -> None: def __init__(self) -> None:
@ -94,31 +105,37 @@ class WorkspaceRegistry():
ws = self._workspaces.inverse.get(w) ws = self._workspaces.inverse.get(w)
return ws if ws else [] return ws if ws else []
def lookupId(self, wid: str) -> Optional[WorkspaceManager]: def lookupParent(self, ws: WorkspaceManager | str) -> sublime.Window:
return next((ws for ws in self._workspaces if ws.id == wid), None) if isinstance(ws, str):
wsm = self.lookupId(ws)
return self._workspaces[ws]
def lookupId(self, wid: str) -> WorkspaceManager:
wsm = next((ws for ws in self._workspaces if ws.id == wid), None)
if not wsm: raise KeyError
return wsm
def add(self, wshandle: codemp.Workspace) -> WorkspaceManager: def add(self, wshandle: codemp.Workspace) -> WorkspaceManager:
win = sublime.active_window() win = sublime.active_window()
tmpdir = tempfile.mkdtemp(prefix="codemp_") # tmpdir = tempfile.mkdtemp(prefix="codemp_")
name = f"{g.WORKSPACE_FOLDER_PREFIX}{wshandle.id()}" # add_project_folder(win, tmpdir, f"{g.WORKSPACE_FOLDER_PREFIX}{wshandle.id()}")
add_project_folder(win, tmpdir, name)
tmpdir = "DISABLED"
wm = WorkspaceManager(wshandle, win, tmpdir) wm = WorkspaceManager(wshandle, win, tmpdir)
self._workspaces[wm] = win self._workspaces[wm] = win
return wm return wm
def remove(self, ws: Optional[WorkspaceManager | str]): def remove(self, ws: WorkspaceManager | str):
if isinstance(ws, str): if isinstance(ws, str):
ws = self.lookupId(ws) ws = self.lookupId(ws)
if not ws: return # remove_project_folder(ws.window, f"{g.WORKSPACE_FOLDER_PREFIX}{ws.id}")
# shutil.rmtree(ws.rootdir, ignore_errors=True)
remove_project_folder(ws.window, f"{g.WORKSPACE_FOLDER_PREFIX}{ws.id}")
shutil.rmtree(ws.rootdir, ignore_errors=True)
del self._workspaces[ws] del self._workspaces[ws]
workspaces = WorkspaceRegistry() workspaces = WorkspaceRegistry()

View file

@ -7,10 +7,9 @@ from typing import Tuple, Union, List
############################################################ ############################################################
class SimpleTextInput(sublime_plugin.TextInputHandler): class SimpleTextInput(sublime_plugin.TextInputHandler):
def __init__(self, *args: Tuple[str, Union[str, List[str]]]): def __init__(self, *args: Tuple[str, Union[str, List[str]]]):
logging.debug(f"why isn't the text input working? {args}") self.input, *self.next_inputs = args
self.argname = args[0][0] self.argname = self.input[0]
self.default = args[0][1] self.default = self.input[1]
self.next_inputs = args[1:]
def initial_text(self): def initial_text(self):
if isinstance(self.default, str): if isinstance(self.default, str):
@ -32,9 +31,9 @@ class SimpleTextInput(sublime_plugin.TextInputHandler):
class SimpleListInput(sublime_plugin.ListInputHandler): class SimpleListInput(sublime_plugin.ListInputHandler):
def __init__(self, *args: Tuple[str, Union["list[str]", str]]): def __init__(self, *args: Tuple[str, Union["list[str]", str]]):
self.argname = args[0][0] self.input, *self.next_inputs = args
self.list = args[0][1] self.argname = self.input[0]
self.next_inputs = args[1:] self.list = self.input[1]
def name(self): def name(self):
return self.argname return self.argname
@ -133,23 +132,23 @@ class SimpleListInput(sublime_plugin.ListInputHandler):
# return AddListEntry(self) # return AddListEntry(self)
class AddListEntry(sublime_plugin.TextInputHandler): # class AddListEntry(sublime_plugin.TextInputHandler):
# this class works when the list input handler # # this class works when the list input handler
# added appended a new element to it's list that will need to be # # added appended a new element to it's list that will need to be
# replaced with the entry added from here! # # replaced with the entry added from here!
def __init__(self, list_input_handler): # def __init__(self, list_input_handler):
self.parent = list_input_handler # self.parent = list_input_handler
def name(self): # def name(self):
return "" # return ""
def validate(self, text: str) -> bool: # def validate(self, text: str) -> bool:
return not len(text) == 0 # return not len(text) == 0
def confirm(self, text: str): # def confirm(self, text: str):
self.parent.list.pop() # removes the add_entry_text # self.parent.list.pop() # removes the add_entry_text
self.parent.list.insert(0, text) # self.parent.list.insert(0, text)
self.parent.preselected = 0 # self.parent.preselected = 0
def next_input(self, args): # def next_input(self, args):
return sublime_plugin.BackInputHandler() # return sublime_plugin.BackInputHandler()

View file

@ -1,6 +1,6 @@
import sublime import sublime
import sublime_plugin import sublime_plugin
from typing import Dict, Generic, TypeVar from typing import Dict, Generic, TypeVar, Optional
from . import globals as g from . import globals as g
# bidirectional dictionary so that we can have bidirectional # bidirectional dictionary so that we can have bidirectional
@ -13,7 +13,6 @@ D = TypeVar("D", Dict, dict)
K = TypeVar("K") K = TypeVar("K")
V = TypeVar("V") V = TypeVar("V")
# using del bd.inverse[key] doesn't work since it can't be intercepted. # using del bd.inverse[key] doesn't work since it can't be intercepted.
# the only way is to iterate: # the only way is to iterate:
# for key in bd.inverse[inverse_key] # for key in bd.inverse[inverse_key]
@ -94,7 +93,7 @@ def populate_view(view, content):
) )
def get_view_from_local_path(path): def view_from_local_path(path):
for window in sublime.windows(): for window in sublime.windows():
for view in window.views(): for view in window.views():
if view.file_name() == path: if view.file_name() == path:
@ -115,3 +114,8 @@ def draw_cursor_region(view, start, end, user):
annotations=[user], # pyright: ignore annotations=[user], # pyright: ignore
annotation_color=g.PALETTE[user_hash % len(g.PALETTE)], annotation_color=g.PALETTE[user_hash % len(g.PALETTE)],
) )
T = TypeVar("T")
def some(x: Optional[T]) -> T:
assert x is not None
return x