mirror of
https://github.com/hexedtech/codemp-sublime.git
synced 2024-11-24 15:54:49 +01:00
BIG CHANGE: fanculo avevo scritto un poema non ho voglia di riscriverlo.
TLDR: updated bindings, "virtual" classes now do less, and only deal with managing the persistence of the codemp object within the editor. the actual commands do the interaction with codemp. moved away from asyncio, now its callbacks spawned on the async sublime thread. the client now is much more central and knows everything. split the join command into join workspace and join buffer, as it was before. simpler and better ux. Former-commit-id: 71c96d321fef2620da4301a8f7af5dff138921cd
This commit is contained in:
parent
507fca5057
commit
1e5aeda755
8 changed files with 625 additions and 513 deletions
|
@ -29,8 +29,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"caption": "Codemp: Join",
|
"caption": "Codemp: Join Workspace",
|
||||||
"command": "codemp_join",
|
"command": "codemp_join_workspace",
|
||||||
|
"arg": {
|
||||||
|
// 'workspace_id': 'asd'
|
||||||
|
// 'buffer_id': 'test'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"caption": "Codemp: Join Buffer",
|
||||||
|
"command": "codemp_join_buffer",
|
||||||
"arg": {
|
"arg": {
|
||||||
// 'workspace_id': 'asd'
|
// 'workspace_id': 'asd'
|
||||||
// 'buffer_id': 'test'
|
// 'buffer_id': 'test'
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
999fd917360a8fa68970c12c70bf5decf30c84a4
|
e674134a5cc02257b28bd8572b9d9e7534c92e5f
|
386
plugin.py
386
plugin.py
|
@ -1,16 +1,14 @@
|
||||||
# pyright: reportIncompatibleMethodOverride=false
|
# pyright: reportIncompatibleMethodOverride=false
|
||||||
|
|
||||||
import sublime
|
import sublime
|
||||||
import sublime_plugin
|
import sublime_plugin
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
|
|
||||||
# from Codemp.src.task_manager import rt
|
import codemp
|
||||||
from Codemp.src.client import client
|
from Codemp.src.client import client
|
||||||
from Codemp.src.utils import safe_listener_detach
|
from Codemp.src.utils import safe_listener_detach
|
||||||
from Codemp.src.utils import safe_listener_attach
|
from Codemp.src.utils import safe_listener_attach
|
||||||
from Codemp.src import globals as g
|
from Codemp.src import globals as g
|
||||||
from codemp import register_logger
|
|
||||||
|
|
||||||
LOG_LEVEL = logging.DEBUG
|
LOG_LEVEL = logging.DEBUG
|
||||||
handler = logging.StreamHandler()
|
handler = logging.StreamHandler()
|
||||||
|
@ -27,9 +25,6 @@ package_logger.propagate = False
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# returns false if logger already exists
|
|
||||||
register_logger(lambda msg: logger.log(logger.level, msg), False)
|
|
||||||
|
|
||||||
TEXT_LISTENER = None
|
TEXT_LISTENER = None
|
||||||
|
|
||||||
|
|
||||||
|
@ -49,37 +44,36 @@ def plugin_unloaded():
|
||||||
safe_listener_detach(TEXT_LISTENER)
|
safe_listener_detach(TEXT_LISTENER)
|
||||||
|
|
||||||
package_logger.removeHandler(handler)
|
package_logger.removeHandler(handler)
|
||||||
client.disconnect()
|
# client.disconnect()
|
||||||
# rt.stop_loop()
|
# rt.stop_loop()
|
||||||
|
|
||||||
|
|
||||||
# Listeners
|
# Listeners
|
||||||
##############################################################################
|
##############################################################################
|
||||||
class EventListener(sublime_plugin.EventListener):
|
class EventListener(sublime_plugin.EventListener):
|
||||||
|
def is_enabled(self):
|
||||||
|
return client.codemp is not None
|
||||||
|
|
||||||
def on_exit(self):
|
def on_exit(self):
|
||||||
client.disconnect()
|
client.disconnect()
|
||||||
|
client.driver.stop()
|
||||||
|
|
||||||
def on_pre_close_window(self, window):
|
def on_pre_close_window(self, window):
|
||||||
if client.active_workspace is None:
|
assert client.codemp is not None
|
||||||
return # nothing to do
|
if not client.valid_window(window):
|
||||||
|
|
||||||
# deactivate all workspaces
|
|
||||||
client.make_active(None)
|
|
||||||
|
|
||||||
s = window.settings()
|
|
||||||
if not s.get(g.CODEMP_WINDOW_TAG, False):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
for wsid in s[g.CODEMP_WINDOW_WORKSPACES]:
|
for vws in client.all_workspaces(window):
|
||||||
ws = client[wsid]
|
client.codemp.leave_workspace(vws.id)
|
||||||
if ws is None:
|
client.uninstall_workspace(vws)
|
||||||
logger.warning(
|
|
||||||
"a tag on the window was found but not a matching workspace."
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
ws.cleanup()
|
def on_text_command(self, view, command_name, args):
|
||||||
del client.workspaces[wsid]
|
if command_name == "codemp_replace_text":
|
||||||
|
logger.info("got a codemp_replace_text command!")
|
||||||
|
|
||||||
|
def on_post_text_command(self, view, command_name, args):
|
||||||
|
if command_name == "codemp_replace_text":
|
||||||
|
logger.info("got a codemp_replace_text command!")
|
||||||
|
|
||||||
|
|
||||||
class CodempClientViewEventListener(sublime_plugin.ViewEventListener):
|
class CodempClientViewEventListener(sublime_plugin.ViewEventListener):
|
||||||
|
@ -92,40 +86,43 @@ class CodempClientViewEventListener(sublime_plugin.ViewEventListener):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def on_selection_modified_async(self):
|
def on_selection_modified_async(self):
|
||||||
ws = client.get_workspace(self.view)
|
region = self.view.sel()[0]
|
||||||
if ws is None:
|
start = self.view.rowcol(region.begin())
|
||||||
return
|
end = self.view.rowcol(region.end())
|
||||||
|
|
||||||
vbuff = ws.get_by_local(self.view.buffer_id())
|
vws = client.workspace_from_view(self.view)
|
||||||
if vbuff is not None:
|
vbuff = client.buffer_from_view(self.view)
|
||||||
vbuff.send_cursor(ws)
|
if vws is None or vbuff is None:
|
||||||
|
raise
|
||||||
|
vws.send_cursor(vbuff.id, start, end)
|
||||||
|
|
||||||
def on_activated(self):
|
def on_activated(self):
|
||||||
# sublime has no proper way to check if a view gained or lost input focus outside of this
|
|
||||||
# callback (i know right?), so we have to manually keep track of which view has the focus
|
|
||||||
g.ACTIVE_CODEMP_VIEW = self.view.id()
|
|
||||||
# print("view {} activated".format(self.view.id()))
|
|
||||||
global TEXT_LISTENER
|
global TEXT_LISTENER
|
||||||
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):
|
||||||
g.ACTIVE_CODEMP_VIEW = None
|
|
||||||
# print("view {} deactivated".format(self.view.id()))
|
|
||||||
global TEXT_LISTENER
|
global TEXT_LISTENER
|
||||||
safe_listener_detach(TEXT_LISTENER) # pyright: ignore
|
safe_listener_detach(TEXT_LISTENER) # pyright: ignore
|
||||||
|
|
||||||
def on_pre_close(self):
|
def on_pre_close(self):
|
||||||
global TEXT_LISTENER
|
if self.view == sublime.active_window().active_view():
|
||||||
if self.view.id() == g.ACTIVE_CODEMP_VIEW:
|
global TEXT_LISTENER
|
||||||
safe_listener_detach(TEXT_LISTENER) # pyright: ignore
|
safe_listener_detach(TEXT_LISTENER) # pyright: ignore
|
||||||
|
|
||||||
ws = client.get_workspace(self.view)
|
vws = client.workspace_from_view(self.view)
|
||||||
if ws is None:
|
vbuff = client.buffer_from_view(self.view)
|
||||||
return
|
if vws is None or vbuff is None:
|
||||||
|
raise
|
||||||
|
|
||||||
vbuff = ws.get_by_local(self.view.buffer_id())
|
vws.uninstall_buffer(vbuff.id)
|
||||||
if vbuff is not None:
|
|
||||||
vbuff.cleanup()
|
def on_text_command(self, command_name, args):
|
||||||
|
if command_name == "codemp_replace_text":
|
||||||
|
logger.info("got a codemp_replace_text command! but in the view listener")
|
||||||
|
|
||||||
|
def on_post_text_command(self, command_name, args):
|
||||||
|
if command_name == "codemp_replace_text":
|
||||||
|
logger.info("got a codemp_replace_text command! but in the view listener")
|
||||||
|
|
||||||
|
|
||||||
class CodempClientTextChangeListener(sublime_plugin.TextChangeListener):
|
class CodempClientTextChangeListener(sublime_plugin.TextChangeListener):
|
||||||
|
@ -135,17 +132,18 @@ class CodempClientTextChangeListener(sublime_plugin.TextChangeListener):
|
||||||
# we'll do it by hand with .attach(buffer).
|
# we'll do it by hand with .attach(buffer).
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# blocking :D
|
# we do the boring stuff in the async thread
|
||||||
def on_text_changed(self, changes):
|
def on_text_changed_async(self, changes):
|
||||||
s = self.buffer.primary_view().settings()
|
s = self.buffer.primary_view().settings()
|
||||||
if s.get(g.CODEMP_IGNORE_NEXT_TEXT_CHANGE, None):
|
if s.get(g.CODEMP_IGNORE_NEXT_TEXT_CHANGE, False):
|
||||||
logger.debug("Ignoring echoing back the change.")
|
logger.debug("Ignoring echoing back the change.")
|
||||||
s[g.CODEMP_IGNORE_NEXT_TEXT_CHANGE] = False
|
s[g.CODEMP_IGNORE_NEXT_TEXT_CHANGE] = False
|
||||||
return
|
return
|
||||||
|
|
||||||
vbuff = client.get_buffer(self.buffer.primary_view())
|
vbuff = client.buffer_from_view(self.buffer.primary_view())
|
||||||
if vbuff is not None:
|
if vbuff is not None:
|
||||||
rt.dispatch(vbuff.send_buffer_change(changes))
|
# but then we block the main one for the actual sending!
|
||||||
|
sublime.set_timeout(lambda: vbuff.send_buffer_change(changes))
|
||||||
|
|
||||||
|
|
||||||
# Commands:
|
# Commands:
|
||||||
|
@ -166,11 +164,23 @@ class CodempClientTextChangeListener(sublime_plugin.TextChangeListener):
|
||||||
# Connect Command
|
# Connect Command
|
||||||
#############################################################################
|
#############################################################################
|
||||||
class CodempConnectCommand(sublime_plugin.WindowCommand):
|
class CodempConnectCommand(sublime_plugin.WindowCommand):
|
||||||
def run(self, server_host, user_name, password="lmaodefaultpassword"):
|
|
||||||
client.connect(server_host, user_name, password)
|
|
||||||
|
|
||||||
def is_enabled(self) -> bool:
|
def is_enabled(self) -> bool:
|
||||||
return client.handle is None
|
return client.codemp is None
|
||||||
|
|
||||||
|
def run(self, server_host, user_name, password="lmaodefaultpassword"):
|
||||||
|
logger.info(f"Connecting to {server_host} with user {user_name}...")
|
||||||
|
|
||||||
|
def try_connect():
|
||||||
|
try:
|
||||||
|
client.connect(server_host, user_name, password)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Could not connect: {e}")
|
||||||
|
sublime.error_message(
|
||||||
|
"Could not connect:\n Make sure the server is up\n\
|
||||||
|
and your credentials are correct."
|
||||||
|
)
|
||||||
|
|
||||||
|
sublime.set_timeout_async(try_connect)
|
||||||
|
|
||||||
def input(self, args):
|
def input(self, args):
|
||||||
if "server_host" not in args:
|
if "server_host" not in args:
|
||||||
|
@ -206,93 +216,148 @@ class ConnectUserName(sublime_plugin.TextInputHandler):
|
||||||
# Separate the join command into two join workspace and join buffer commands that get called back to back
|
# Separate the join command into two join workspace and join buffer commands that get called back to back
|
||||||
|
|
||||||
|
|
||||||
# Generic Join Command
|
# Generic Join Workspace Command
|
||||||
#############################################################################
|
#############################################################################
|
||||||
class CodempJoinCommand(sublime_plugin.WindowCommand):
|
class CodempJoinWorkspaceCommand(sublime_plugin.WindowCommand):
|
||||||
def run(self, workspace_id, buffer_id):
|
|
||||||
if workspace_id == "":
|
|
||||||
return
|
|
||||||
|
|
||||||
vws = client.workspaces.get(workspace_id)
|
|
||||||
if vws is None:
|
|
||||||
try:
|
|
||||||
vws = client.join_workspace(workspace_id)
|
|
||||||
except Exception as e:
|
|
||||||
raise e
|
|
||||||
|
|
||||||
if vws is None:
|
|
||||||
logger.warning("The client returned a void workspace.")
|
|
||||||
return
|
|
||||||
|
|
||||||
vws.materialize()
|
|
||||||
|
|
||||||
if buffer_id == "* Don't Join Any":
|
|
||||||
buffer_id = ""
|
|
||||||
|
|
||||||
if buffer_id != "":
|
|
||||||
vws.attach(buffer_id)
|
|
||||||
|
|
||||||
def is_enabled(self) -> bool:
|
def is_enabled(self) -> bool:
|
||||||
return client.handle is not None
|
return client.codemp is not None
|
||||||
|
|
||||||
|
def run(self, workspace_id):
|
||||||
|
assert client.codemp is not None
|
||||||
|
if client.valid_workspace(workspace_id):
|
||||||
|
logger.info(f"Joining workspace: '{workspace_id}'...")
|
||||||
|
promise = client.codemp.join_workspace(workspace_id)
|
||||||
|
active_window = sublime.active_window()
|
||||||
|
|
||||||
|
def defer_instantiation(promise):
|
||||||
|
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
|
||||||
|
client.install_workspace(workspace, active_window)
|
||||||
|
|
||||||
|
sublime.set_timeout_async(lambda: defer_instantiation(promise))
|
||||||
|
# the else shouldn't really happen, and if it does, it should already be instantiated.
|
||||||
|
# ignore.
|
||||||
|
|
||||||
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:
|
||||||
return JoinWorkspaceIdList()
|
return WorkspaceIdText()
|
||||||
|
|
||||||
|
|
||||||
class JoinWorkspaceIdList(sublime_plugin.ListInputHandler):
|
class WorkspaceIdText(sublime_plugin.TextInputHandler):
|
||||||
# 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.
|
|
||||||
def __init__(self):
|
|
||||||
self.list = client.active_workspaces()
|
|
||||||
self.list.sort()
|
|
||||||
self.list.append("* Create New...")
|
|
||||||
self.preselected = None
|
|
||||||
|
|
||||||
def name(self):
|
def name(self):
|
||||||
return "workspace_id"
|
return "workspace_id"
|
||||||
|
|
||||||
def placeholder(self):
|
|
||||||
return "Workspace"
|
|
||||||
|
|
||||||
def list_items(self):
|
# To allow for having a selection and choosing non existing workspaces
|
||||||
if self.preselected is not None:
|
# we do a little dance: We pass this list input handler to a TextInputHandler
|
||||||
return (self.list, self.preselected)
|
# when we select "Create New..." which adds his result to the list of possible
|
||||||
else:
|
# workspaces and pop itself off the stack to go back to the list handler.
|
||||||
return self.list
|
# class WorkspaceIdList(sublime_plugin.ListInputHandler):
|
||||||
|
# def __init__(self):
|
||||||
|
# assert client.codemp is not None # the command should not be available
|
||||||
|
|
||||||
def next_input(self, args):
|
# # at the moment, the client can't give us a full list of existing workspaces
|
||||||
if args["workspace_id"] == "* Create New...":
|
# # so a textinputhandler would be more appropriate. but we keep this for the future
|
||||||
return AddListEntryName(self)
|
|
||||||
|
|
||||||
wid = args["workspace_id"]
|
# self.add_entry_text = "* add entry..."
|
||||||
if wid != "":
|
# self.list = client.codemp.active_workspaces()
|
||||||
vws = client.join_workspace(wid)
|
# self.list.sort()
|
||||||
else:
|
# self.list.append(self.add_entry_text)
|
||||||
vws = None
|
# self.preselected = None
|
||||||
try:
|
|
||||||
return ListBufferId(vws)
|
# def name(self):
|
||||||
except Exception:
|
# return "workspace_id"
|
||||||
return TextBufferId()
|
|
||||||
|
# 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 TextBufferId(sublime_plugin.TextInputHandler):
|
class CodempJoinBufferCommand(sublime_plugin.WindowCommand):
|
||||||
def name(self):
|
def is_enabled(self):
|
||||||
return "buffer_id"
|
available_workspaces = client.all_workspaces(self.window)
|
||||||
|
return len(available_workspaces) > 0
|
||||||
|
|
||||||
|
def run(self, workspace_id, buffer_id):
|
||||||
|
# A workspace has some Buffers inside of it (filetree)
|
||||||
|
# some of those you are already attached to
|
||||||
|
# If already attached to it return the same alredy existing bufferctl
|
||||||
|
# if existing but not attached (attach)
|
||||||
|
# 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?
|
||||||
|
if vws.valid_buffer(buffer_id):
|
||||||
|
return # do nothing.
|
||||||
|
|
||||||
|
if buffer_id not in vws.codemp.filetree(filter=buffer_id):
|
||||||
|
create = sublime.ok_cancel_dialog(
|
||||||
|
"There is no buffer named '{buffer_id}' in the workspace '{workspace_id}'.\n\
|
||||||
|
Do you want to create it?",
|
||||||
|
ok_title="yes",
|
||||||
|
title="Create Buffer?",
|
||||||
|
)
|
||||||
|
if create:
|
||||||
|
try:
|
||||||
|
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
|
||||||
|
promise = vws.codemp.attach(buffer_id)
|
||||||
|
|
||||||
|
def deferred_attach(promise):
|
||||||
|
try:
|
||||||
|
buff_ctl = promise.wait()
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"error when attaching to buffer '{id}':\n\n {e}")
|
||||||
|
sublime.error_message(f"Could not attach to buffer '{buffer_id}'")
|
||||||
|
return
|
||||||
|
vbuff = vws.install_buffer(buff_ctl)
|
||||||
|
# TODO! if the view is already active calling focus_view() will not trigger the on_activate
|
||||||
|
self.window.focus_view(vbuff.view)
|
||||||
|
|
||||||
|
sublime.set_timeout_async(lambda: deferred_attach(promise))
|
||||||
|
|
||||||
|
def input_description(self) -> str:
|
||||||
|
return "Attach: "
|
||||||
|
|
||||||
|
def input(self, args):
|
||||||
|
# if we have only a workspace in the window, then
|
||||||
|
# skip to the buffer choice
|
||||||
|
if "workspace_id" not in args:
|
||||||
|
return ActiveWorkspacesIdList(self.window, get_buffer=True)
|
||||||
|
|
||||||
|
if "buffer_id" not in args:
|
||||||
|
return BufferIdList(args["workspace_id"])
|
||||||
|
|
||||||
|
|
||||||
class ListBufferId(sublime_plugin.ListInputHandler):
|
class BufferIdList(sublime_plugin.ListInputHandler):
|
||||||
def __init__(self, vws):
|
def __init__(self, workspace_id):
|
||||||
self.ws = vws
|
self.add_entry_text = "* create new..."
|
||||||
self.list = vws.handle.filetree()
|
self.list = [vbuff.id for vbuff in client.all_buffers(workspace_id)]
|
||||||
self.list.sort()
|
self.list.sort()
|
||||||
self.list.append("* Create New...")
|
self.list.append(self.add_entry_text)
|
||||||
self.list.append("* Don't Join Any")
|
|
||||||
self.preselected = None
|
self.preselected = None
|
||||||
|
|
||||||
def name(self):
|
def name(self):
|
||||||
|
@ -307,34 +372,9 @@ class ListBufferId(sublime_plugin.ListInputHandler):
|
||||||
else:
|
else:
|
||||||
return self.list
|
return self.list
|
||||||
|
|
||||||
def cancel(self):
|
|
||||||
client.leave_workspace(self.ws.id)
|
|
||||||
|
|
||||||
def next_input(self, args):
|
def next_input(self, args):
|
||||||
if args["buffer_id"] == "* Create New...":
|
if args["buffer_id"] == self.add_entry_text:
|
||||||
return AddListEntryName(self)
|
return AddListEntry(self)
|
||||||
|
|
||||||
if args["buffer_id"] == "* Dont' Join Any":
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class AddListEntryName(sublime_plugin.TextInputHandler):
|
|
||||||
def __init__(self, list_handler):
|
|
||||||
self.parent = list_handler
|
|
||||||
|
|
||||||
def name(self):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def validate(self, text: str) -> bool:
|
|
||||||
return not len(text) == 0
|
|
||||||
|
|
||||||
def confirm(self, text: str):
|
|
||||||
self.parent.list.pop() # removes the "Create New..."
|
|
||||||
self.parent.list.insert(0, text)
|
|
||||||
self.parent.preselected = 0
|
|
||||||
|
|
||||||
def next_input(self, args):
|
|
||||||
return sublime_plugin.BackInputHandler()
|
|
||||||
|
|
||||||
|
|
||||||
# Text Change Command
|
# Text Change Command
|
||||||
|
@ -363,11 +403,8 @@ class CodempReplaceTextCommand(sublime_plugin.TextCommand):
|
||||||
# Disconnect Command
|
# Disconnect Command
|
||||||
#############################################################################
|
#############################################################################
|
||||||
class CodempDisconnectCommand(sublime_plugin.WindowCommand):
|
class CodempDisconnectCommand(sublime_plugin.WindowCommand):
|
||||||
def is_enabled(self) -> bool:
|
def is_enabled(self):
|
||||||
if client.handle is not None:
|
return client.codemp is not None
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
client.disconnect()
|
client.disconnect()
|
||||||
|
@ -375,23 +412,54 @@ class CodempDisconnectCommand(sublime_plugin.WindowCommand):
|
||||||
|
|
||||||
# Leave Workspace Command
|
# Leave Workspace Command
|
||||||
class CodempLeaveWorkspaceCommand(sublime_plugin.WindowCommand):
|
class CodempLeaveWorkspaceCommand(sublime_plugin.WindowCommand):
|
||||||
def is_enabled(self) -> bool:
|
def is_enabled(self):
|
||||||
return client.handle is not None and len(client.workspaces.keys()) > 0
|
return client.codemp is not None and len(client.all_workspaces(self.window)) > 0
|
||||||
|
|
||||||
def run(self, id: str):
|
def run(self, workspace_id: str):
|
||||||
client.leave_workspace(id)
|
# client.leave_workspace(id)
|
||||||
|
pass
|
||||||
|
|
||||||
def input(self, args):
|
def input(self, args):
|
||||||
if "id" not in args:
|
if "id" not in args:
|
||||||
return LeaveWorkspaceIdList()
|
return ActiveWorkspacesIdList()
|
||||||
|
|
||||||
|
|
||||||
class LeaveWorkspaceIdList(sublime_plugin.ListInputHandler):
|
class ActiveWorkspacesIdList(sublime_plugin.ListInputHandler):
|
||||||
|
def __init__(self, window=None, get_buffer=False):
|
||||||
|
self.window = window
|
||||||
|
self.get_buffer = get_buffer
|
||||||
|
|
||||||
def name(self):
|
def name(self):
|
||||||
return "id"
|
return "workspace_id"
|
||||||
|
|
||||||
def list_items(self):
|
def list_items(self):
|
||||||
return client.active_workspaces()
|
return [vws.id for vws in client.all_workspaces(self.window)]
|
||||||
|
|
||||||
|
def next_input(self, args):
|
||||||
|
if self.get_buffer:
|
||||||
|
return BufferIdList(args["workspace_id"])
|
||||||
|
|
||||||
|
|
||||||
|
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 None
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
# Proxy Commands ( NOT USED, left just in case we need it again. )
|
# Proxy Commands ( NOT USED, left just in case we need it again. )
|
||||||
|
|
134
src/buffers.py
134
src/buffers.py
|
@ -1,11 +1,9 @@
|
||||||
import sublime
|
import sublime
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
from asyncio import CancelledError
|
|
||||||
|
|
||||||
from codemp import BufferController
|
import codemp
|
||||||
from Codemp.src import globals as g
|
from Codemp.src import globals as g
|
||||||
from Codemp.src.task_manager import rt
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -17,58 +15,88 @@ logger = logging.getLogger(__name__)
|
||||||
class VirtualBuffer:
|
class VirtualBuffer:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
workspace_id: str,
|
buffctl: codemp.BufferController,
|
||||||
workspace_rootdir: str,
|
view: sublime.View, # noqa: F821 # type: ignore
|
||||||
remote_id: str,
|
|
||||||
buffctl: BufferController,
|
|
||||||
):
|
):
|
||||||
self.view = sublime.active_window().new_file()
|
|
||||||
self.codemp_id = remote_id
|
|
||||||
self.sublime_id = self.view.buffer_id()
|
|
||||||
self.workspace_id = workspace_id
|
|
||||||
self.workspace_rootdir = workspace_rootdir
|
|
||||||
self.buffctl = buffctl
|
self.buffctl = buffctl
|
||||||
|
self.view = view
|
||||||
|
self.id = self.buffctl.name()
|
||||||
|
|
||||||
self.tmpfile = os.path.join(workspace_rootdir, self.codemp_id)
|
def __hash__(self) -> int:
|
||||||
|
return hash(self.id)
|
||||||
|
|
||||||
self.view.set_name(self.codemp_id)
|
def cleanup(self):
|
||||||
|
self.uninstall()
|
||||||
|
self.buffctl.stop()
|
||||||
|
|
||||||
|
def install(self, rootdir):
|
||||||
|
if self.installed:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.tmpfile = os.path.join(rootdir, self.id)
|
||||||
open(self.tmpfile, "a").close()
|
open(self.tmpfile, "a").close()
|
||||||
self.view.retarget(self.tmpfile)
|
|
||||||
self.view.set_scratch(True)
|
self.view.set_scratch(True)
|
||||||
|
self.view.set_name(self.id)
|
||||||
|
self.view.retarget(self.tmpfile)
|
||||||
|
|
||||||
rt.dispatch(
|
|
||||||
self.apply_bufferchange_task(),
|
|
||||||
f"{g.BUFFCTL_TASK_PREFIX}-{self.codemp_id}",
|
|
||||||
)
|
|
||||||
|
|
||||||
# mark the view as a codemp view
|
|
||||||
s = self.view.settings()
|
s = self.view.settings()
|
||||||
self.view.set_status(g.SUBLIME_STATUS_ID, "[Codemp]")
|
self.view.set_status(g.SUBLIME_STATUS_ID, "[Codemp]")
|
||||||
s[g.CODEMP_BUFFER_TAG] = True
|
s[g.CODEMP_BUFFER_TAG] = True
|
||||||
s[g.CODEMP_REMOTE_ID] = self.codemp_id
|
|
||||||
s[g.CODEMP_WORKSPACE_ID] = self.workspace_id
|
|
||||||
|
|
||||||
def cleanup(self):
|
self.__activate()
|
||||||
|
|
||||||
|
self.installed = True
|
||||||
|
|
||||||
|
def uninstall(self):
|
||||||
|
if not self.installed:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.__deactivate()
|
||||||
|
|
||||||
os.remove(self.tmpfile)
|
os.remove(self.tmpfile)
|
||||||
# cleanup views
|
|
||||||
s = self.view.settings()
|
s = self.view.settings()
|
||||||
del s[g.CODEMP_BUFFER_TAG]
|
del s[g.CODEMP_BUFFER_TAG]
|
||||||
del s[g.CODEMP_REMOTE_ID]
|
|
||||||
del s[g.CODEMP_WORKSPACE_ID]
|
|
||||||
self.view.erase_status(g.SUBLIME_STATUS_ID)
|
self.view.erase_status(g.SUBLIME_STATUS_ID)
|
||||||
|
|
||||||
rt.stop_task(f"{g.BUFFCTL_TASK_PREFIX}-{self.codemp_id}")
|
self.installed = False
|
||||||
self.buffctl.stop()
|
|
||||||
logger.info(f"cleaning up virtual buffer '{self.codemp_id}'")
|
|
||||||
|
|
||||||
async def apply_bufferchange_task(self):
|
def __activate(self):
|
||||||
logger.debug(f"spinning up '{self.codemp_id}' buffer worker...")
|
logger.info(f"registering a callback for buffer: {self.id}")
|
||||||
try:
|
self.buffctl.callback(self.__apply_bufferchange_cb)
|
||||||
while text_change := await self.buffctl.recv():
|
self.isactive = True
|
||||||
change_id = self.view.change_id()
|
|
||||||
if text_change.is_empty():
|
def __deactivate(self):
|
||||||
|
logger.info(f"clearing a callback for buffer: {self.id}")
|
||||||
|
self.buffctl.clear_callback()
|
||||||
|
self.isactive = False
|
||||||
|
|
||||||
|
def send_buffer_change(self, changes):
|
||||||
|
# we do not do any index checking, and trust sublime with providing the correct
|
||||||
|
# sequential indexing, assuming the changes are applied in the order they are received.
|
||||||
|
for change in changes:
|
||||||
|
region = sublime.Region(change.a.pt, change.b.pt)
|
||||||
|
logger.debug(
|
||||||
|
"sending txt change: Reg({} {}) -> '{}'".format(
|
||||||
|
region.begin(), region.end(), change.str
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|
||||||
|
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.")
|
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.
|
||||||
|
@ -81,37 +109,11 @@ class VirtualBuffer:
|
||||||
self.view.run_command(
|
self.view.run_command(
|
||||||
"codemp_replace_text",
|
"codemp_replace_text",
|
||||||
{
|
{
|
||||||
"start": text_change.start,
|
"start": change.start,
|
||||||
"end": text_change.end,
|
"end": change.end,
|
||||||
"content": text_change.content,
|
"content": change.content,
|
||||||
"change_id": change_id,
|
"change_id": change_id,
|
||||||
}, # pyright: ignore
|
}, # pyright: ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
except CancelledError:
|
sublime.set_timeout(get_change_and_apply)
|
||||||
logger.debug(f"'{self.codemp_id}' buffer worker stopped...")
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"buffer worker '{self.codemp_id}' crashed:\n{e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def send_buffer_change(self, changes):
|
|
||||||
# we do not do any index checking, and trust sublime with providing the correct
|
|
||||||
# sequential indexing, assuming the changes are applied in the order they are received.
|
|
||||||
for change in changes:
|
|
||||||
region = sublime.Region(change.a.pt, change.b.pt)
|
|
||||||
logger.debug(
|
|
||||||
"sending txt change: Reg({} {}) -> '{}'".format(
|
|
||||||
region.begin(), region.end(), change.str
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.buffctl.send(region.begin(), region.end(), change.str)
|
|
||||||
|
|
||||||
def send_cursor(self, vws): # pyright: ignore # noqa: F821
|
|
||||||
# TODO: only the last placed cursor/selection.
|
|
||||||
# status_log(f"sending cursor position in workspace: {vbuff.workspace.id}")
|
|
||||||
region = self.view.sel()[0]
|
|
||||||
start = self.view.rowcol(region.begin()) # only counts UTF8 chars
|
|
||||||
end = self.view.rowcol(region.end())
|
|
||||||
|
|
||||||
vws.curctl.send(self.codemp_id, start, end)
|
|
||||||
|
|
177
src/client.py
177
src/client.py
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Optional
|
from typing import Optional, Dict
|
||||||
|
|
||||||
|
|
||||||
import sublime
|
import sublime
|
||||||
import logging
|
import logging
|
||||||
|
@ -7,114 +8,132 @@ import logging
|
||||||
import codemp
|
import codemp
|
||||||
from Codemp.src import globals as g
|
from Codemp.src import globals as g
|
||||||
from Codemp.src.workspace import VirtualWorkspace
|
from Codemp.src.workspace import VirtualWorkspace
|
||||||
|
from Codemp.src.buffers import VirtualBuffer
|
||||||
|
from Codemp.src.utils import bidict
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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:
|
class VirtualClient:
|
||||||
handle: Optional[codemp.Client]
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
self.codemp: Optional[codemp.Client] = None
|
||||||
self.driver = codemp.init(lambda msg: logger.log(logger.level, msg), False)
|
self.driver = codemp.init(lambda msg: logger.log(logger.level, msg), False)
|
||||||
self.workspaces: dict[str, VirtualWorkspace] = {}
|
|
||||||
self.active_workspace: Optional[None] = None
|
|
||||||
|
|
||||||
def __getitem__(self, key: str):
|
# bookkeeping corner
|
||||||
return self.workspaces.get(key)
|
self.__id2buffer: dict[str, VirtualBuffer] = {}
|
||||||
|
self.__id2workspace: dict[str, VirtualWorkspace] = {}
|
||||||
|
self.__view2buff: dict[sublime.View, VirtualBuffer] = {}
|
||||||
|
|
||||||
|
self.__buff2workspace: bidict[VirtualBuffer, VirtualWorkspace] = bidict()
|
||||||
|
self.__workspace2window: bidict[VirtualWorkspace, sublime.Window] = bidict()
|
||||||
|
|
||||||
|
def valid_window(self, window: sublime.Window):
|
||||||
|
return window in self.__workspace2window.inverse
|
||||||
|
|
||||||
|
def valid_workspace(self, workspace: VirtualWorkspace | str):
|
||||||
|
if isinstance(workspace, str):
|
||||||
|
return client.__id2workspace.get(workspace) is not None
|
||||||
|
|
||||||
|
return workspace in self.__workspace2window
|
||||||
|
|
||||||
|
def all_workspaces(
|
||||||
|
self, window: Optional[sublime.Window] = None
|
||||||
|
) -> list[VirtualWorkspace]:
|
||||||
|
if window is None:
|
||||||
|
return list(self.__workspace2window.keys())
|
||||||
|
else:
|
||||||
|
return self.__workspace2window.inverse[window]
|
||||||
|
|
||||||
|
def workspace_from_view(self, view: sublime.View) -> Optional[VirtualWorkspace]:
|
||||||
|
buff = self.__view2buff.get(view, None)
|
||||||
|
return self.__buff2workspace.get(buff, None)
|
||||||
|
|
||||||
|
def workspace_from_buffer(self, buff: VirtualBuffer) -> Optional[VirtualWorkspace]:
|
||||||
|
return self.__buff2workspace.get(buff)
|
||||||
|
|
||||||
|
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.__buff2workspace.keys())
|
||||||
|
else:
|
||||||
|
if isinstance(workspace, str):
|
||||||
|
workspace = client.__id2workspace[workspace]
|
||||||
|
return self.__buff2workspace.inverse[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 disconnect(self):
|
def disconnect(self):
|
||||||
if self.handle is None:
|
if self.codemp is None:
|
||||||
return
|
return
|
||||||
logger.info("disconnecting from the current client")
|
logger.info("disconnecting from the current client")
|
||||||
for vws in self.workspaces.values():
|
# for each workspace tell it to clean up after itself.
|
||||||
|
for vws in self.all_workspaces():
|
||||||
vws.cleanup()
|
vws.cleanup()
|
||||||
|
self.codemp.leave_workspace(vws.id)
|
||||||
|
|
||||||
self.handle = None
|
self.__id2workspace.clear()
|
||||||
|
self.__id2buffer.clear()
|
||||||
|
self.__buff2workspace.clear()
|
||||||
|
self.__view2buff.clear()
|
||||||
|
self.__workspace2window.clear()
|
||||||
|
self.codemp = None
|
||||||
|
|
||||||
def connect(self, host: str, user: str, password: str):
|
def connect(self, host: str, user: str, password: str):
|
||||||
if self.handle is not None:
|
if self.codemp is not None:
|
||||||
logger.info("Disconnecting from previous client.")
|
logger.info("Disconnecting from previous client.")
|
||||||
return self.disconnect()
|
return self.disconnect()
|
||||||
|
|
||||||
logger.info(f"Connecting to {host} with user {user}")
|
self.codemp = codemp.Client(host, user, password)
|
||||||
try:
|
id = self.codemp.user_id()
|
||||||
self.handle = codemp.Client(host, user, password)
|
logger.debug(f"Connected to '{host}' as user {user} (id: {id})")
|
||||||
|
|
||||||
if self.handle is not None:
|
def install_workspace(
|
||||||
id = self.handle.user_id()
|
self, workspace: codemp.Workspace, window: sublime.Window
|
||||||
logger.debug(f"Connected to '{host}' with user {user} and id: {id}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Could not connect: {e}")
|
|
||||||
sublime.error_message(
|
|
||||||
"Could not connect:\n Make sure the server is up.\n\
|
|
||||||
or your credentials are correct."
|
|
||||||
)
|
|
||||||
raise
|
|
||||||
|
|
||||||
def join_workspace(
|
|
||||||
self,
|
|
||||||
workspace_id: str,
|
|
||||||
) -> VirtualWorkspace:
|
) -> VirtualWorkspace:
|
||||||
if self.handle is None:
|
# we pass the window as well so if the window changes in the mean
|
||||||
sublime.error_message("Connect to a server first.")
|
# time we have the correct one!
|
||||||
raise
|
vws = VirtualWorkspace(workspace, window)
|
||||||
|
self.__workspace2window[vws] = window
|
||||||
|
self.__id2workspace[vws.id] = vws
|
||||||
|
|
||||||
logger.info(f"Joining workspace: '{workspace_id}'")
|
vws.install()
|
||||||
try:
|
|
||||||
workspace = self.handle.join_workspace(workspace_id).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}'")
|
|
||||||
raise
|
|
||||||
|
|
||||||
vws = VirtualWorkspace(workspace)
|
|
||||||
self.workspaces[workspace_id] = vws
|
|
||||||
|
|
||||||
return vws
|
return vws
|
||||||
|
|
||||||
def leave_workspace(self, id: str):
|
def uninstall_workspace(self, vws: VirtualWorkspace):
|
||||||
if self.handle is None:
|
if vws not in self.__workspace2window:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
if self.handle.leave_workspace(id):
|
logger.info(f"Uninstalling workspace '{vws.id}'...")
|
||||||
logger.info(f"Leaving workspace: '{id}'")
|
vws.cleanup()
|
||||||
self.workspaces[id].cleanup()
|
del self.__id2workspace[vws.id]
|
||||||
del self.workspaces[id]
|
del self.__workspace2window[vws]
|
||||||
|
self.__buff2workspace.inverse_del(vws)
|
||||||
|
|
||||||
def get_workspace(self, view):
|
def workspaces_in_server(self):
|
||||||
tag_id = view.settings().get(g.CODEMP_WORKSPACE_ID)
|
return self.codemp.active_workspaces() if self.codemp else []
|
||||||
if tag_id is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
ws = self.workspaces.get(tag_id)
|
|
||||||
if ws is None:
|
|
||||||
logging.warning("a tag on the view was found but not a matching workspace.")
|
|
||||||
return
|
|
||||||
|
|
||||||
return ws
|
|
||||||
|
|
||||||
def active_workspaces(self):
|
|
||||||
return self.handle.active_workspaces() if self.handle else []
|
|
||||||
|
|
||||||
def user_id(self):
|
def user_id(self):
|
||||||
return self.handle.user_id() if self.handle else None
|
return self.codemp.user_id() if self.codemp else None
|
||||||
|
|
||||||
def get_buffer(self, view):
|
|
||||||
ws = self.get_workspace(view)
|
|
||||||
return None if ws is None else ws.get_by_local(view.buffer_id())
|
|
||||||
|
|
||||||
def make_active(self, ws: Optional[VirtualWorkspace]):
|
|
||||||
if self.active_workspace == ws:
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.active_workspace is not None:
|
|
||||||
self.active_workspace.deactivate()
|
|
||||||
|
|
||||||
if ws is not None:
|
|
||||||
ws.activate()
|
|
||||||
|
|
||||||
self.active_workspace = ws # pyright: ignore
|
|
||||||
|
|
||||||
|
|
||||||
client = VirtualClient()
|
client = VirtualClient()
|
||||||
|
|
|
@ -6,8 +6,6 @@ import asyncio
|
||||||
import threading
|
import threading
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
|
|
||||||
# from ..ext import sublime_asyncio as rt
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
76
src/utils.py
76
src/utils.py
|
@ -1,7 +1,58 @@
|
||||||
import sublime
|
import sublime
|
||||||
import sublime_plugin
|
import sublime_plugin
|
||||||
|
from typing import Dict, Generic, TypeVar
|
||||||
from Codemp.src import globals as g
|
from Codemp.src import globals as g
|
||||||
|
|
||||||
|
# bidirectional dictionary so that we can have bidirectional
|
||||||
|
# lookup!
|
||||||
|
# In particular we can use it for:
|
||||||
|
# bd[workspace_id] = window
|
||||||
|
# bd[view] = virtual_buffer
|
||||||
|
|
||||||
|
D = TypeVar("D", Dict, dict)
|
||||||
|
K = TypeVar("K")
|
||||||
|
V = TypeVar("V")
|
||||||
|
|
||||||
|
|
||||||
|
# using del bd.inverse[key] doesn't work since it can't be intercepted.
|
||||||
|
# the only way is to iterate:
|
||||||
|
# for key in bd.inverse[inverse_key]
|
||||||
|
class bidict(dict, Generic[K, V]):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.inverse: Dict[V, list[K]] = {}
|
||||||
|
|
||||||
|
for key, value in self.items():
|
||||||
|
self.inverse.setdefault(value, []).append(key)
|
||||||
|
|
||||||
|
def __setitem__(self, key: K, value: V):
|
||||||
|
if key in self:
|
||||||
|
self.inverse[self[key]].remove(key)
|
||||||
|
super(bidict, self).__setitem__(key, value)
|
||||||
|
self.inverse.setdefault(value, []).append(key)
|
||||||
|
|
||||||
|
def __delitem__(self, key: K):
|
||||||
|
# if we delete a normal key, remove the key from the inverse element.
|
||||||
|
inverse_key = self[key]
|
||||||
|
self.inverse.setdefault(inverse_key, []).remove(key)
|
||||||
|
|
||||||
|
# if the resulting inverse key list is empty delete it
|
||||||
|
if inverse_key in self.inverse and not self.inverse[inverse_key]:
|
||||||
|
del self.inverse[inverse_key]
|
||||||
|
|
||||||
|
# delete the normal key
|
||||||
|
super(bidict, self).__delitem__(key)
|
||||||
|
|
||||||
|
def inverse_del(self, inverse_key: V):
|
||||||
|
# deletes all the elements matching the inverse key
|
||||||
|
# the last del will also delete the inverse key.
|
||||||
|
for key in self.inverse[inverse_key]:
|
||||||
|
self.pop(key, None)
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self.inverse.clear()
|
||||||
|
super(bidict, self).clear()
|
||||||
|
|
||||||
|
|
||||||
def status_log(msg, popup=False):
|
def status_log(msg, popup=False):
|
||||||
sublime.status_message("[codemp] {}".format(msg))
|
sublime.status_message("[codemp] {}".format(msg))
|
||||||
|
@ -50,20 +101,17 @@ def get_view_from_local_path(path):
|
||||||
return view
|
return view
|
||||||
|
|
||||||
|
|
||||||
def draw_cursor_region(view, cursor):
|
def draw_cursor_region(view, start, end, user):
|
||||||
reg = rowcol_to_region(view, cursor.start, cursor.end)
|
reg = rowcol_to_region(view, start, end)
|
||||||
reg_flags = sublime.RegionFlags.DRAW_EMPTY
|
reg_flags = sublime.RegionFlags.DRAW_EMPTY
|
||||||
|
|
||||||
user_hash = hash(cursor.user)
|
user_hash = hash(user)
|
||||||
|
|
||||||
def draw():
|
view.add_regions(
|
||||||
view.add_regions(
|
f"{g.SUBLIME_REGIONS_PREFIX}-{user_hash}",
|
||||||
f"{g.SUBLIME_REGIONS_PREFIX}-{user_hash}",
|
[reg],
|
||||||
[reg],
|
flags=reg_flags,
|
||||||
flags=reg_flags,
|
scope=g.REGIONS_COLORS[user_hash % len(g.REGIONS_COLORS)],
|
||||||
scope=g.REGIONS_COLORS[user_hash % len(g.REGIONS_COLORS)],
|
annotations=[user], # pyright: ignore
|
||||||
annotations=[cursor.user], # pyright: ignore
|
annotation_color=g.PALETTE[user_hash % len(g.PALETTE)],
|
||||||
annotation_color=g.PALETTE[user_hash % len(g.PALETTE)],
|
)
|
||||||
)
|
|
||||||
|
|
||||||
sublime.set_timeout_async(draw)
|
|
||||||
|
|
349
src/workspace.py
349
src/workspace.py
|
@ -1,19 +1,16 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Optional
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
import sublime
|
import sublime
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
import logging
|
import logging
|
||||||
from asyncio import CancelledError
|
|
||||||
|
|
||||||
from codemp import Workspace, Promise, CursorController
|
|
||||||
|
|
||||||
|
import codemp
|
||||||
from Codemp.src import globals as g
|
from Codemp.src import globals as g
|
||||||
from Codemp.src.buffers import VirtualBuffer
|
from Codemp.src.buffers import VirtualBuffer
|
||||||
|
|
||||||
# from Codemp.src.task_manager import rt
|
|
||||||
from Codemp.src.utils import draw_cursor_region
|
from Codemp.src.utils import draw_cursor_region
|
||||||
|
from Codemp.src.utils import bidict
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -22,235 +19,207 @@ logger = logging.getLogger(__name__)
|
||||||
# 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:
|
||||||
def __init__(self, handle: Workspace):
|
def __init__(self, handle: codemp.Workspace, window: sublime.Window):
|
||||||
self.handle: Workspace = handle
|
self.codemp: codemp.Workspace = handle
|
||||||
self.id: str = self.handle.id()
|
self.window: sublime.Window = window
|
||||||
self.sublime_window: sublime.Window = sublime.active_window()
|
self.curctl: codemp.CursorController = self.codemp.cursor()
|
||||||
self.curctl: CursorController = handle.cursor()
|
|
||||||
self.materialized = False
|
self.id: str = self.codemp.id()
|
||||||
self.isactive = False
|
|
||||||
|
self.codemp.fetch_buffers()
|
||||||
|
self.codemp.fetch_users()
|
||||||
|
|
||||||
# mapping remote ids -> local ids
|
# mapping remote ids -> local ids
|
||||||
self.id_map: dict[str, int] = {}
|
self.__buff2view: bidict[VirtualBuffer, sublime.View] = bidict()
|
||||||
self.active_buffers: dict[int, VirtualBuffer] = {} # local_id -> VBuff
|
self.__id2buff: dict[str, VirtualBuffer] = {}
|
||||||
|
# self.id_map: dict[str, int] = {}
|
||||||
|
# self.active_buffers: dict[int, VirtualBuffer] = {} # local_id -> VBuff
|
||||||
|
|
||||||
def _cleanup(self):
|
def __hash__(self) -> int:
|
||||||
self._deactivate()
|
# so we can use these as dict keys!
|
||||||
|
return hash(self.id)
|
||||||
|
|
||||||
# the worskpace only cares about closing the various open views on its Buffer.
|
def sync(self):
|
||||||
|
# check that the state we have here is the same as the one codemp has internally!
|
||||||
|
# if not get up to speed!
|
||||||
|
self.codemp.fetch_buffers().wait()
|
||||||
|
attached_buffers = self.codemp.buffer_list()
|
||||||
|
all(id in self.__id2buff for id in attached_buffers)
|
||||||
|
# TODO!
|
||||||
|
|
||||||
|
def valid_bufffer(self, buff: VirtualBuffer | str):
|
||||||
|
if isinstance(buff, str):
|
||||||
|
return self.buff_by_id(buff) is not None
|
||||||
|
|
||||||
|
return buff in self.__buff2view
|
||||||
|
|
||||||
|
def all_buffers(self) -> list[VirtualBuffer]:
|
||||||
|
return list(self.__buff2view.keys())
|
||||||
|
|
||||||
|
def buff_by_view(self, view: sublime.View) -> Optional[VirtualBuffer]:
|
||||||
|
buff = self.__buff2view.inverse.get(view)
|
||||||
|
return buff[0] if buff is not None else None
|
||||||
|
|
||||||
|
def buff_by_id(self, id: str) -> Optional[VirtualBuffer]:
|
||||||
|
return self.__id2buff.get(id)
|
||||||
|
|
||||||
|
def all_views(self) -> list[sublime.View]:
|
||||||
|
return list(self.__buff2view.inverse.keys())
|
||||||
|
|
||||||
|
def view_by_buffer(self, buffer: VirtualBuffer) -> sublime.View:
|
||||||
|
return buffer.view
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
# the worskpace only cares about closing the various open views of its buffers.
|
||||||
# the event listener calls the cleanup code for each buffer independently on its own
|
# the event listener calls the cleanup code for each buffer independently on its own
|
||||||
# upon closure.
|
# upon closure.
|
||||||
for vbuff in self.active_buffers.values():
|
for view in self.all_views():
|
||||||
vbuff.view.close()
|
view.close()
|
||||||
|
|
||||||
self.active_buffers = {} # drop all Buffer, let them be garbage collected (hopefully)
|
self.__buff2view.clear()
|
||||||
|
self.__id2buff.clear()
|
||||||
|
|
||||||
if not self.materialized:
|
self.uninstall()
|
||||||
return # nothing to delete
|
self.curctl.stop()
|
||||||
|
|
||||||
# remove from the "virtual" project folders
|
def uninstall(self):
|
||||||
d: dict = self.sublime_window.project_data() # pyright: ignore
|
if not self.installed:
|
||||||
if d is None:
|
return
|
||||||
|
|
||||||
|
self.__deactivate()
|
||||||
|
|
||||||
|
proj: dict = self.window.project_data() # type:ignore
|
||||||
|
if proj is None:
|
||||||
raise
|
raise
|
||||||
newf = list(
|
|
||||||
|
clean_proj_folders = list(
|
||||||
filter(
|
filter(
|
||||||
lambda f: f.get("name", "") != f"{g.WORKSPACE_FOLDER_PREFIX}{self.id}",
|
lambda f: f.get("name", "") != f"{g.WORKSPACE_FOLDER_PREFIX}{self.id}",
|
||||||
d["folders"],
|
proj["folders"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
d["folders"] = newf
|
proj["folders"] = clean_proj_folders
|
||||||
self.sublime_window.set_project_data(d)
|
self.window.set_project_data(proj)
|
||||||
logger.info(f"cleaning up virtual workspace '{self.id}'")
|
logger.info(f"cleaning up virtual workspace '{self.id}'")
|
||||||
shutil.rmtree(self.rootdir, ignore_errors=True)
|
shutil.rmtree(self.rootdir, ignore_errors=True)
|
||||||
|
|
||||||
# stop the controller
|
self.installed = False
|
||||||
self.curctl.stop()
|
|
||||||
|
|
||||||
# clean the window form the tags
|
def install(self):
|
||||||
s = self.sublime_window.settings()
|
if self.installed:
|
||||||
del s[g.CODEMP_WINDOW_TAG]
|
return
|
||||||
del s[g.CODEMP_WINDOW_WORKSPACES]
|
|
||||||
|
|
||||||
self.materialized = False
|
|
||||||
|
|
||||||
def _materialize(self):
|
|
||||||
# attach the workspace to the editor, tagging windows and populating
|
|
||||||
# virtual file systems
|
|
||||||
if self.materialized:
|
|
||||||
return # no op, we already have materialized the workspace in the editor
|
|
||||||
|
|
||||||
# initialise the virtual filesystem
|
# initialise the virtual filesystem
|
||||||
tmpdir = tempfile.mkdtemp(prefix="codemp_")
|
tmpdir = tempfile.mkdtemp(prefix="codemp_")
|
||||||
logging.debug(f"setting up virtual fs for workspace in: {tmpdir}")
|
logging.debug(f"setting up virtual fs for workspace in: {tmpdir}")
|
||||||
self.rootdir = tmpdir
|
self.rootdir = tmpdir
|
||||||
|
|
||||||
# and add a new "project folder"
|
proj: dict = self.window.project_data() # pyright: ignore
|
||||||
proj_data: dict = self.sublime_window.project_data() # pyright: ignore
|
if proj is None:
|
||||||
if proj_data is None:
|
proj = {"folders": []} # pyright: ignore, Value can be None
|
||||||
proj_data = {"folders": []}
|
|
||||||
|
|
||||||
proj_data["folders"].append(
|
proj["folders"].append(
|
||||||
{"name": f"{g.WORKSPACE_FOLDER_PREFIX}{self.id}", "path": self.rootdir}
|
{"name": f"{g.WORKSPACE_FOLDER_PREFIX}{self.id}", "path": self.rootdir}
|
||||||
)
|
)
|
||||||
self.sublime_window.set_project_data(proj_data)
|
self.window.set_project_data(proj)
|
||||||
|
|
||||||
s: dict = self.sublime_window.settings() # pyright: ignore
|
self.__activate()
|
||||||
if s.get(g.CODEMP_WINDOW_TAG, False):
|
self.installed = True
|
||||||
s[g.CODEMP_WINDOW_WORKSPACES].append(self.id)
|
|
||||||
else:
|
|
||||||
s[g.CODEMP_WINDOW_TAG] = True
|
|
||||||
s[g.CODEMP_WINDOW_WORKSPACES] = [self.id]
|
|
||||||
|
|
||||||
self.materialized = True
|
def __activate(self):
|
||||||
|
self.curctl.callback(self.__move_cursor_callback)
|
||||||
# def _activate(self):
|
self.isactive = True
|
||||||
# rt.dispatch(
|
|
||||||
# self.move_cursor_task(),
|
|
||||||
# f"{g.CURCTL_TASK_PREFIX}-{self.id}",
|
|
||||||
# )
|
|
||||||
# self.isactive = True
|
|
||||||
|
|
||||||
def _deactivate(self):
|
|
||||||
if self.isactive:
|
|
||||||
rt.stop_task(f"{g.CURCTL_TASK_PREFIX}-{self.id}")
|
|
||||||
|
|
||||||
|
def __deactivate(self):
|
||||||
|
self.curctl.clear_callback()
|
||||||
self.isactive = False
|
self.isactive = False
|
||||||
|
|
||||||
def _add_buffer(self, remote_id: str, vbuff: VirtualBuffer):
|
def install_buffer(self, buff: codemp.BufferController) -> VirtualBuffer:
|
||||||
self.id_map[remote_id] = vbuff.view.buffer_id()
|
view = self.window.new_file()
|
||||||
self.active_buffers[vbuff.view.buffer_id()] = vbuff
|
|
||||||
|
|
||||||
def _get_by_local(self, local_id: int) -> Optional[VirtualBuffer]:
|
vbuff = VirtualBuffer(buff, view)
|
||||||
return self.active_buffers.get(local_id)
|
self.__buff2view[vbuff] = view
|
||||||
|
self.__id2buff[vbuff.id] = vbuff
|
||||||
|
|
||||||
def _get_by_remote(self, remote_id: str) -> Optional[VirtualBuffer]:
|
vbuff.install(self.rootdir)
|
||||||
local_id = self.id_map.get(remote_id)
|
|
||||||
if local_id is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
vbuff = self.active_buffers.get(local_id)
|
|
||||||
if vbuff is None:
|
|
||||||
logging.warning(
|
|
||||||
"a local-remote buffer id pair was found but \
|
|
||||||
not the matching virtual buffer."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
return vbuff
|
return vbuff
|
||||||
|
|
||||||
def create(self, id: str) -> Promise[None]:
|
def uninstall_buffer(self, vbuff: VirtualBuffer):
|
||||||
return self.handle.create(id)
|
vbuff.cleanup()
|
||||||
|
buffview = self.view_by_buffer(vbuff)
|
||||||
|
del self.__buff2view[vbuff]
|
||||||
|
del self.__id2buff[vbuff.id]
|
||||||
|
buffview.close()
|
||||||
|
|
||||||
# A workspace has some Buffer inside of it (filetree)
|
# def detach(self, id: str):
|
||||||
# some of those you are already attached to (Buffer_by_name)
|
# attached_Buffer = self.codemp.buffer_by_name(id)
|
||||||
# If already attached to it return the same alredy existing bufferctl
|
# if attached_Buffer is None:
|
||||||
# if existing but not attached (attach)
|
# sublime.error_message(f"You are not attached to the buffer '{id}'")
|
||||||
# if not existing ask for creation (create + attach)
|
# logging.warning(f"You are not attached to the buffer '{id}'")
|
||||||
def attach(self, id: str):
|
# return
|
||||||
if id is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
attached_Buffer = self.handle.buffer_by_name(id)
|
# self.codemp.detach(id)
|
||||||
if attached_Buffer is not None:
|
|
||||||
return self._get_by_remote(id)
|
|
||||||
|
|
||||||
self.handle.fetch_buffers()
|
# def delete(self, id: str):
|
||||||
existing_Buffer = self.handle.filetree(filter=None)
|
# self.codemp.fetch_buffers()
|
||||||
if id not in existing_Buffer:
|
# existing_Buffer = self.codemp.filetree(filter=None)
|
||||||
create = sublime.ok_cancel_dialog(
|
# if id not in existing_Buffer:
|
||||||
"There is no buffer named '{id}' in the workspace.\n\
|
# sublime.error_message(f"The buffer '{id}' does not exists.")
|
||||||
Do you want to create it?",
|
# logging.info(f"The buffer '{id}' does not exists.")
|
||||||
ok_title="yes",
|
# return
|
||||||
title="Create Buffer?",
|
# # delete a buffer that exists but you are not attached to
|
||||||
)
|
# attached_Buffer = self.codemp.buffer_by_name(id)
|
||||||
if create:
|
# if attached_Buffer is None:
|
||||||
try:
|
# delete = sublime.ok_cancel_dialog(
|
||||||
create_promise = self.create(id)
|
# "Confirm you want to delete the buffer '{id}'",
|
||||||
except Exception as e:
|
# ok_title="delete",
|
||||||
logging.error(f"could not create buffer:\n\n {e}")
|
# title="Delete Buffer?",
|
||||||
return
|
# )
|
||||||
create_promise.wait()
|
# if delete:
|
||||||
else:
|
# try:
|
||||||
return
|
# self.codemp.delete(id).wait()
|
||||||
|
# except Exception as e:
|
||||||
|
# logging.error(
|
||||||
|
# f"error when deleting the buffer '{id}':\n\n {e}", True
|
||||||
|
# )
|
||||||
|
# return
|
||||||
|
# else:
|
||||||
|
# return
|
||||||
|
|
||||||
# now either we created it or it exists already
|
# # delete buffer that you are attached to
|
||||||
try:
|
# delete = sublime.ok_cancel_dialog(
|
||||||
buff_ctl = self.handle.attach(id)
|
# "Confirm you want to delete the buffer '{id}'.\n\
|
||||||
except Exception as e:
|
# You will be disconnected from it.",
|
||||||
logging.error(f"error when attaching to buffer '{id}':\n\n {e}")
|
# ok_title="delete",
|
||||||
return
|
# title="Delete Buffer?",
|
||||||
|
# )
|
||||||
|
# if delete:
|
||||||
|
# self.codemp.detach(id)
|
||||||
|
# try:
|
||||||
|
# self.codemp.delete(id).wait()
|
||||||
|
# except Exception as e:
|
||||||
|
# logging.error(f"error when deleting the buffer '{id}':\n\n {e}", True)
|
||||||
|
# return
|
||||||
|
|
||||||
vbuff = VirtualBuffer(self.id, self.rootdir, id, buff_ctl.wait())
|
def send_cursor(self, id: str, start: Tuple[int, int], end: Tuple[int, int]):
|
||||||
self._add_buffer(id, vbuff)
|
# 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)
|
||||||
|
|
||||||
# TODO! if the view is already active calling focus_view() will not trigger the on_activate
|
def __move_cursor_callback(self, ctl: codemp.CursorController):
|
||||||
self.sublime_window.focus_view(vbuff.view)
|
def get_event_and_draw():
|
||||||
|
while event := ctl.try_recv().wait():
|
||||||
def detach(self, id: str):
|
if event is None:
|
||||||
attached_Buffer = self.handle.buffer_by_name(id)
|
break
|
||||||
if attached_Buffer 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
|
|
||||||
|
|
||||||
self.handle.detach(id)
|
|
||||||
|
|
||||||
def delete(self, id: str):
|
|
||||||
self.handle.fetch_buffers()
|
|
||||||
existing_Buffer = self.handle.filetree(filter=None)
|
|
||||||
if id not in existing_Buffer:
|
|
||||||
sublime.error_message(f"The buffer '{id}' does not exists.")
|
|
||||||
logging.info(f"The buffer '{id}' does not exists.")
|
|
||||||
return
|
|
||||||
# delete a buffer that exists but you are not attached to
|
|
||||||
attached_Buffer = self.handle.buffer_by_name(id)
|
|
||||||
if attached_Buffer is None:
|
|
||||||
delete = sublime.ok_cancel_dialog(
|
|
||||||
"Confirm you want to delete the buffer '{id}'",
|
|
||||||
ok_title="delete",
|
|
||||||
title="Delete Buffer?",
|
|
||||||
)
|
|
||||||
if delete:
|
|
||||||
try:
|
|
||||||
self.handle.delete(id).wait()
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(
|
|
||||||
f"error when deleting the buffer '{id}':\n\n {e}", True
|
|
||||||
)
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
# delete buffer that you are attached to
|
|
||||||
delete = sublime.ok_cancel_dialog(
|
|
||||||
"Confirm you want to delete the buffer '{id}'.\n\
|
|
||||||
You will be disconnected from it.",
|
|
||||||
ok_title="delete",
|
|
||||||
title="Delete Buffer?",
|
|
||||||
)
|
|
||||||
if delete:
|
|
||||||
self.handle.detach(id)
|
|
||||||
try:
|
|
||||||
self.handle.delete(id).wait()
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"error when deleting the buffer '{id}':\n\n {e}", True)
|
|
||||||
return
|
|
||||||
|
|
||||||
async def move_cursor_task(self):
|
|
||||||
logger.debug(f"spinning up cursor worker for workspace '{self.id}'...")
|
|
||||||
try:
|
|
||||||
# blocking for now ...
|
|
||||||
while cursor_event := self.curctl.recv().wait():
|
|
||||||
vbuff = self._get_by_remote(cursor_event.buffer)
|
|
||||||
|
|
||||||
|
vbuff = self.buff_by_id(event.buffer)
|
||||||
if vbuff is None:
|
if vbuff is None:
|
||||||
|
logger.warning(
|
||||||
|
"received a cursor event for a buffer that wasn't saved internally."
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
draw_cursor_region(vbuff.view, cursor_event)
|
draw_cursor_region(vbuff.view, event.start, event.end, event.user)
|
||||||
|
|
||||||
except CancelledError:
|
sublime.set_timeout_async(get_event_and_draw)
|
||||||
logger.debug(f"cursor worker for '{self.id}' stopped...")
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"cursor worker '{self.id}' crashed:\n{e}")
|
|
||||||
raise
|
|
||||||
|
|
Loading…
Reference in a new issue