mirror of
https://github.com/hexedtech/codemp-sublime.git
synced 2024-11-21 22:34:48 +01:00
feat: major refactor.
the client doesn't hold the task manager anymore, instead it is now a singleton that all objects dump their tasks into to be managed. This allows to better separate the responsibilities of the various virtual-objects. The client now is also a module-singleton which allows us to remove that ugly global variable. fixup: uses relative imports instead that absolute ones with Codemp in front. rename of codemp_client.py into just client.py Former-commit-id: 4a0cc20e82f9631931ba2f61379a61c461b1f291
This commit is contained in:
parent
b66b368907
commit
705c5e014e
5 changed files with 260 additions and 198 deletions
115
plugin.py
115
plugin.py
|
@ -5,71 +5,80 @@ import sublime_plugin
|
||||||
# import sys
|
# import sys
|
||||||
# import importlib.util
|
# import importlib.util
|
||||||
|
|
||||||
from .src.codemp_client import VirtualClient
|
from .src.TaskManager import tm
|
||||||
from .src.codemp_client import CodempLogger
|
from .src.client import client, VirtualClient
|
||||||
from .src.TaskManager import rt
|
from .src.client import CodempLogger
|
||||||
from .src.utils import status_log
|
from .src.utils import status_log
|
||||||
from .src.utils import safe_listener_detach
|
from .src.utils import safe_listener_detach
|
||||||
|
from .src.utils import safe_listener_attach
|
||||||
from .src import globals as g
|
from .src import globals as g
|
||||||
from .bindings.codemp import init_logger
|
from .bindings.codemp import init_logger
|
||||||
|
|
||||||
CLIENT = None
|
|
||||||
TEXT_LISTENER = None
|
TEXT_LISTENER = None
|
||||||
|
|
||||||
|
|
||||||
# Initialisation and Deinitialisation
|
# Initialisation and Deinitialisation
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
|
||||||
def plugin_loaded():
|
def plugin_loaded():
|
||||||
global CLIENT
|
|
||||||
global TEXT_LISTENER
|
global TEXT_LISTENER
|
||||||
|
|
||||||
# instantiate and start a global asyncio event loop.
|
# instantiate and start a global asyncio event loop.
|
||||||
# pass in the exit_handler coroutine that will be called upon relasing the event loop.
|
# pass in the exit_handler coroutine that will be called upon relasing the event loop.
|
||||||
CLIENT = VirtualClient(disconnect_client)
|
tm.acquire(disconnect_client)
|
||||||
TEXT_LISTENER = CodempClientTextChangeListener()
|
|
||||||
|
|
||||||
logger = CodempLogger(init_logger(False))
|
logger = CodempLogger(init_logger(False))
|
||||||
CLIENT.tm.dispatch(logger.spawn_logger(), "codemp-logger")
|
tm.dispatch(logger.log(), "codemp-logger")
|
||||||
|
|
||||||
|
TEXT_LISTENER = CodempClientTextChangeListener()
|
||||||
|
|
||||||
status_log("plugin loaded")
|
status_log("plugin loaded")
|
||||||
|
|
||||||
|
|
||||||
async def disconnect_client():
|
async def disconnect_client():
|
||||||
global CLIENT
|
|
||||||
global TEXT_LISTENER
|
global TEXT_LISTENER
|
||||||
|
|
||||||
safe_listener_detach(TEXT_LISTENER)
|
tm.stop_all()
|
||||||
CLIENT.tm.stop_all()
|
|
||||||
|
|
||||||
for vws in CLIENT.workspaces.values():
|
if TEXT_LISTENER is not None:
|
||||||
|
safe_listener_detach(TEXT_LISTENER)
|
||||||
|
|
||||||
|
for vws in client.workspaces.values():
|
||||||
vws.cleanup()
|
vws.cleanup()
|
||||||
|
|
||||||
|
|
||||||
def plugin_unloaded():
|
def plugin_unloaded():
|
||||||
global CLIENT
|
|
||||||
# releasing the runtime, runs the disconnect callback defined when acquiring the event loop.
|
# releasing the runtime, runs the disconnect callback defined when acquiring the event loop.
|
||||||
CLIENT.tm.release(False)
|
status_log("unloading")
|
||||||
|
tm.release(False)
|
||||||
|
|
||||||
|
|
||||||
# Listeners
|
# Listeners
|
||||||
##############################################################################
|
##############################################################################
|
||||||
class EventListener(sublime_plugin.EventListener):
|
class EventListener(sublime_plugin.EventListener):
|
||||||
def on_exit(self):
|
def on_exit(self):
|
||||||
global CLIENT
|
tm.release(True)
|
||||||
CLIENT.tm.release(True)
|
|
||||||
|
|
||||||
def on_pre_close_window(self, window):
|
def on_pre_close_window(self, window):
|
||||||
global CLIENT
|
if client.active_workspace is None:
|
||||||
|
return # nothing to do
|
||||||
|
|
||||||
|
client.make_active(None)
|
||||||
|
|
||||||
s = window.settings()
|
s = window.settings()
|
||||||
if s.get(g.CODEMP_WINDOW_TAG, False):
|
if not s.get(g.CODEMP_WINDOW_TAG, False):
|
||||||
for wsid in s[g.CODEMP_WINDOW_WORKSPACES]:
|
return
|
||||||
ws = CLIENT[wsid]
|
|
||||||
if ws is not None:
|
for wsid in s[g.CODEMP_WINDOW_WORKSPACES]:
|
||||||
if ws.id == CLIENT.active_workspace.id:
|
ws = client[wsid]
|
||||||
CLIENT.active_workspace = None
|
if ws is None:
|
||||||
CLIENT.tm.stop(f"{g.CURCTL_TASK_PREFIX}-{ws.id}")
|
status_log(
|
||||||
ws.cleanup()
|
"[WARN] a tag on the window was found but not a matching workspace."
|
||||||
del CLIENT.workspaces[wsid]
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
ws.cleanup()
|
||||||
|
del client.workspaces[wsid]
|
||||||
|
|
||||||
|
|
||||||
class CodempClientViewEventListener(sublime_plugin.ViewEventListener):
|
class CodempClientViewEventListener(sublime_plugin.ViewEventListener):
|
||||||
|
@ -82,12 +91,13 @@ class CodempClientViewEventListener(sublime_plugin.ViewEventListener):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def on_selection_modified_async(self):
|
def on_selection_modified_async(self):
|
||||||
s = self.view.settings()
|
ws = client.get_workspace(self.view)
|
||||||
|
if ws is None:
|
||||||
|
return
|
||||||
|
|
||||||
global CLIENT
|
vbuff = ws.get_by_local(self.view.buffer_id())
|
||||||
vbuff = CLIENT[s[g.CODEMP_WORKSPACE_ID]].get_by_local(self.view.buffer_id())
|
|
||||||
if vbuff is not None:
|
if vbuff is not None:
|
||||||
CLIENT.send_cursor(vbuff)
|
vbuff.send_cursor(ws)
|
||||||
|
|
||||||
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
|
# sublime has no proper way to check if a view gained or lost input focus outside of this
|
||||||
|
@ -95,7 +105,7 @@ class CodempClientViewEventListener(sublime_plugin.ViewEventListener):
|
||||||
g.ACTIVE_CODEMP_VIEW = self.view.id()
|
g.ACTIVE_CODEMP_VIEW = self.view.id()
|
||||||
# print("view {} activated".format(self.view.id()))
|
# print("view {} activated".format(self.view.id()))
|
||||||
global TEXT_LISTENER
|
global TEXT_LISTENER
|
||||||
TEXT_LISTENER.attach(self.view.buffer())
|
safe_listener_attach(TEXT_LISTENER, self.view.buffer())
|
||||||
|
|
||||||
def on_deactivated(self):
|
def on_deactivated(self):
|
||||||
g.ACTIVE_CODEMP_VIEW = None
|
g.ACTIVE_CODEMP_VIEW = None
|
||||||
|
@ -108,12 +118,13 @@ class CodempClientViewEventListener(sublime_plugin.ViewEventListener):
|
||||||
if self.view.id() == g.ACTIVE_CODEMP_VIEW:
|
if self.view.id() == g.ACTIVE_CODEMP_VIEW:
|
||||||
safe_listener_detach(TEXT_LISTENER)
|
safe_listener_detach(TEXT_LISTENER)
|
||||||
|
|
||||||
global CLIENT
|
ws = client.get_workspace(self.view)
|
||||||
wsid = self.view.settings().get(g.CODEMP_WORKSPACE_ID)
|
if ws is None:
|
||||||
vbuff = CLIENT[wsid].get_by_local(self.view.buffer_id())
|
return
|
||||||
vbuff.cleanup()
|
|
||||||
|
|
||||||
CLIENT.tm.stop(f"{g.BUFFCTL_TASK_PREFIX}-{vbuff.codemp_id}")
|
vbuff = ws.get_by_local(self.view.buffer_id())
|
||||||
|
if vbuff is not None:
|
||||||
|
vbuff.cleanup()
|
||||||
|
|
||||||
|
|
||||||
class CodempClientTextChangeListener(sublime_plugin.TextChangeListener):
|
class CodempClientTextChangeListener(sublime_plugin.TextChangeListener):
|
||||||
|
@ -131,9 +142,9 @@ class CodempClientTextChangeListener(sublime_plugin.TextChangeListener):
|
||||||
s[g.CODEMP_IGNORE_NEXT_TEXT_CHANGE] = False
|
s[g.CODEMP_IGNORE_NEXT_TEXT_CHANGE] = False
|
||||||
return
|
return
|
||||||
|
|
||||||
global CLIENT
|
vbuff = client.get_buffer(self.buffer.primary_view())
|
||||||
vbuff = CLIENT[s[g.CODEMP_WORKSPACE_ID]].get_by_local(self.buffer.id())
|
if vbuff is not None:
|
||||||
CLIENT.send_buffer_change(changes, vbuff)
|
vbuff.send_buffer_change(changes)
|
||||||
|
|
||||||
|
|
||||||
# Commands:
|
# Commands:
|
||||||
|
@ -153,8 +164,7 @@ class CodempClientTextChangeListener(sublime_plugin.TextChangeListener):
|
||||||
#############################################################################
|
#############################################################################
|
||||||
class CodempConnectCommand(sublime_plugin.WindowCommand):
|
class CodempConnectCommand(sublime_plugin.WindowCommand):
|
||||||
def run(self, server_host):
|
def run(self, server_host):
|
||||||
global CLIENT
|
tm.dispatch(client.connect(server_host))
|
||||||
rt.dispatch(CLIENT.connect(server_host))
|
|
||||||
|
|
||||||
def input(self, args):
|
def input(self, args):
|
||||||
if "server_host" not in args:
|
if "server_host" not in args:
|
||||||
|
@ -174,8 +184,7 @@ async def JoinCommand(client: VirtualClient, workspace_id: str, buffer_id: str):
|
||||||
|
|
||||||
class CodempJoinCommand(sublime_plugin.WindowCommand):
|
class CodempJoinCommand(sublime_plugin.WindowCommand):
|
||||||
def run(self, workspace_id, buffer_id):
|
def run(self, workspace_id, buffer_id):
|
||||||
global CLIENT
|
tm.dispatch(JoinCommand(client, workspace_id, buffer_id))
|
||||||
rt.dispatch(JoinCommand(CLIENT, workspace_id, buffer_id))
|
|
||||||
|
|
||||||
def input_description(self):
|
def input_description(self):
|
||||||
return "Join:"
|
return "Join:"
|
||||||
|
@ -189,8 +198,7 @@ class CodempJoinCommand(sublime_plugin.WindowCommand):
|
||||||
#############################################################################
|
#############################################################################
|
||||||
class CodempJoinWorkspaceCommand(sublime_plugin.WindowCommand):
|
class CodempJoinWorkspaceCommand(sublime_plugin.WindowCommand):
|
||||||
def run(self, workspace_id):
|
def run(self, workspace_id):
|
||||||
global CLIENT
|
tm.dispatch(client.join_workspace(workspace_id, "sublime3"))
|
||||||
rt.dispatch(CLIENT.join_workspace(workspace_id, "sublime3"))
|
|
||||||
|
|
||||||
def input_description(self):
|
def input_description(self):
|
||||||
return "Join specific workspace"
|
return "Join specific workspace"
|
||||||
|
@ -204,23 +212,21 @@ class CodempJoinWorkspaceCommand(sublime_plugin.WindowCommand):
|
||||||
#############################################################################
|
#############################################################################
|
||||||
class CodempJoinBufferCommand(sublime_plugin.WindowCommand):
|
class CodempJoinBufferCommand(sublime_plugin.WindowCommand):
|
||||||
def run(self, buffer_id):
|
def run(self, buffer_id):
|
||||||
global CLIENT
|
if client.active_workspace is None:
|
||||||
if CLIENT.active_workspace is None:
|
|
||||||
sublime.error_message(
|
sublime.error_message(
|
||||||
"You haven't joined any worksapce yet. \
|
"You haven't joined any worksapce yet. \
|
||||||
use `Codemp: Join Workspace` or `Codemp: Join`"
|
use `Codemp: Join Workspace` or `Codemp: Join`"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
rt.dispatch(CLIENT.active_workspace.attach(buffer_id))
|
tm.dispatch(client.active_workspace.attach(buffer_id))
|
||||||
|
|
||||||
def input_description(self):
|
def input_description(self):
|
||||||
return "Join buffer in the active workspace"
|
return "Join buffer in the active workspace"
|
||||||
|
|
||||||
# This is awful, fix it
|
# This is awful, fix it
|
||||||
def input(self, args):
|
def input(self, args):
|
||||||
global CLIENT
|
if client.active_workspace is None:
|
||||||
if CLIENT.active_workspace is None:
|
|
||||||
sublime.error_message(
|
sublime.error_message(
|
||||||
"You haven't joined any worksapce yet. \
|
"You haven't joined any worksapce yet. \
|
||||||
use `Codemp: Join Workspace` or `Codemp: Join`"
|
use `Codemp: Join Workspace` or `Codemp: Join`"
|
||||||
|
@ -228,7 +234,7 @@ class CodempJoinBufferCommand(sublime_plugin.WindowCommand):
|
||||||
return
|
return
|
||||||
|
|
||||||
if "buffer_id" not in args:
|
if "buffer_id" not in args:
|
||||||
existing_buffers = CLIENT.active_workspace.handle.filetree()
|
existing_buffers = client.active_workspace.handle.filetree()
|
||||||
if len(existing_buffers) == 0:
|
if len(existing_buffers) == 0:
|
||||||
return RawBufferId()
|
return RawBufferId()
|
||||||
else:
|
else:
|
||||||
|
@ -259,8 +265,7 @@ class ListBufferId(sublime_plugin.ListInputHandler):
|
||||||
return "buffer_id"
|
return "buffer_id"
|
||||||
|
|
||||||
def list_items(self):
|
def list_items(self):
|
||||||
global CLIENT
|
return client.active_workspace.handle.filetree()
|
||||||
return CLIENT.active_workspace.handle.filetree()
|
|
||||||
|
|
||||||
def next_input(self, args):
|
def next_input(self, args):
|
||||||
if "buffer_id" not in args:
|
if "buffer_id" not in args:
|
||||||
|
@ -313,7 +318,7 @@ class RawBufferId(sublime_plugin.TextInputHandler):
|
||||||
#############################################################################
|
#############################################################################
|
||||||
class CodempDisconnectCommand(sublime_plugin.WindowCommand):
|
class CodempDisconnectCommand(sublime_plugin.WindowCommand):
|
||||||
def run(self):
|
def run(self):
|
||||||
rt.sync(disconnect_client())
|
tm.sync(disconnect_client())
|
||||||
|
|
||||||
|
|
||||||
# Proxy Commands ( NOT USED, left just in case we need it again. )
|
# Proxy Commands ( NOT USED, left just in case we need it again. )
|
||||||
|
|
|
@ -1,21 +1,29 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import asyncio
|
import asyncio
|
||||||
import Codemp.ext.sublime_asyncio as rt
|
from ..ext import sublime_asyncio as rt
|
||||||
|
|
||||||
|
|
||||||
class TaskManager:
|
class TaskManager:
|
||||||
def __init__(self, exit_handler):
|
def __init__(self):
|
||||||
self.tasks = []
|
self.tasks = []
|
||||||
self.exit_handler_id = rt.acquire(exit_handler)
|
self.runtime = rt
|
||||||
|
self.exit_handler_id = None
|
||||||
|
|
||||||
|
def acquire(self, exit_handler):
|
||||||
|
if self.exit_handler_id is None:
|
||||||
|
# don't allow multiple exit handlers
|
||||||
|
self.exit_handler_id = self.runtime.acquire(exit_handler)
|
||||||
|
|
||||||
|
return self.exit_handler_id
|
||||||
|
|
||||||
def release(self, at_exit):
|
def release(self, at_exit):
|
||||||
rt.release(at_exit=at_exit, exit_handler_id=self.exit_handler_id)
|
self.runtime.release(at_exit=at_exit, exit_handler_id=self.exit_handler_id)
|
||||||
|
|
||||||
def dispatch(self, coro, name):
|
def dispatch(self, coro, name=None):
|
||||||
rt.dispatch(coro, self.store_named_lambda(name))
|
self.runtime.dispatch(coro, self.store_named_lambda(name))
|
||||||
|
|
||||||
def sync(self, coro):
|
def sync(self, coro):
|
||||||
rt.sync(coro)
|
self.runtime.sync(coro)
|
||||||
|
|
||||||
def remove_stopped(self):
|
def remove_stopped(self):
|
||||||
self.tasks = list(filter(lambda T: not T.cancelled(), self.tasks))
|
self.tasks = list(filter(lambda T: not T.cancelled(), self.tasks))
|
||||||
|
@ -26,7 +34,7 @@ class TaskManager:
|
||||||
self.tasks.append(task)
|
self.tasks.append(task)
|
||||||
self.remove_stopped()
|
self.remove_stopped()
|
||||||
|
|
||||||
def store_named_lambda(self, name):
|
def store_named_lambda(self, name=None):
|
||||||
def _store(task):
|
def _store(task):
|
||||||
self.store(task, name)
|
self.store(task, name)
|
||||||
|
|
||||||
|
@ -43,7 +51,7 @@ class TaskManager:
|
||||||
def pop_task(self, name) -> Optional:
|
def pop_task(self, name) -> Optional:
|
||||||
idx = self.get_task_idx(name)
|
idx = self.get_task_idx(name)
|
||||||
if id is not None:
|
if id is not None:
|
||||||
return self.task.pop(idx)
|
return self.tasks.pop(idx)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def _stop(self, task):
|
async def _stop(self, task):
|
||||||
|
@ -56,8 +64,12 @@ class TaskManager:
|
||||||
def stop(self, name):
|
def stop(self, name):
|
||||||
t = self.get_task(name)
|
t = self.get_task(name)
|
||||||
if t is not None:
|
if t is not None:
|
||||||
rt.dispatch(self._stop(t))
|
self.runtime.dispatch(self._stop(t))
|
||||||
|
|
||||||
def stop_all(self):
|
def stop_all(self):
|
||||||
for task in self.tasks:
|
for task in self.tasks:
|
||||||
rt.dispatch(self._stop(task))
|
self.runtime.dispatch(self._stop(task))
|
||||||
|
|
||||||
|
|
||||||
|
# singleton instance
|
||||||
|
tm = TaskManager()
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Optional, Callable
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
import sublime
|
import sublime
|
||||||
import asyncio
|
import asyncio
|
||||||
|
@ -8,11 +7,10 @@ import tempfile
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
|
from ..src import globals as g
|
||||||
import Codemp.src.globals as g
|
from ..src.TaskManager import tm
|
||||||
from Codemp.src.wrappers import BufferController, Workspace, Client
|
from ..src.wrappers import BufferController, Workspace, Client
|
||||||
from Codemp.src.utils import status_log, rowcol_to_region
|
from ..src.utils import status_log, rowcol_to_region
|
||||||
from Codemp.src.TaskManager import TaskManager
|
|
||||||
|
|
||||||
|
|
||||||
class CodempLogger:
|
class CodempLogger:
|
||||||
|
@ -22,7 +20,7 @@ class CodempLogger:
|
||||||
async def message(self):
|
async def message(self):
|
||||||
return await self.handle.message()
|
return await self.handle.message()
|
||||||
|
|
||||||
async def spawn_logger(self):
|
async def log(self):
|
||||||
status_log("spinning up the logger...")
|
status_log("spinning up the logger...")
|
||||||
try:
|
try:
|
||||||
while msg := await self.handle.message():
|
while msg := await self.handle.message():
|
||||||
|
@ -44,12 +42,11 @@ class VirtualBuffer:
|
||||||
self,
|
self,
|
||||||
workspace: VirtualWorkspace,
|
workspace: VirtualWorkspace,
|
||||||
remote_id: str,
|
remote_id: str,
|
||||||
view: sublime.View,
|
|
||||||
buffctl: BufferController,
|
buffctl: BufferController,
|
||||||
):
|
):
|
||||||
self.view = view
|
self.view = sublime.active_window().new_file()
|
||||||
self.codemp_id = remote_id
|
self.codemp_id = remote_id
|
||||||
self.sublime_id = view.buffer_id()
|
self.sublime_id = self.view.buffer_id()
|
||||||
|
|
||||||
self.workspace = workspace
|
self.workspace = workspace
|
||||||
self.buffctl = buffctl
|
self.buffctl = buffctl
|
||||||
|
@ -61,6 +58,11 @@ class VirtualBuffer:
|
||||||
self.view.retarget(self.tmpfile)
|
self.view.retarget(self.tmpfile)
|
||||||
self.view.set_scratch(True)
|
self.view.set_scratch(True)
|
||||||
|
|
||||||
|
tm.dispatch(
|
||||||
|
self.apply_bufferchange_task(),
|
||||||
|
f"{g.BUFFCTL_TASK_PREFIX}-{self.codemp_id}",
|
||||||
|
)
|
||||||
|
|
||||||
# mark the view as a codemp view
|
# 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]")
|
||||||
|
@ -76,20 +78,74 @@ class VirtualBuffer:
|
||||||
del s[g.CODEMP_REMOTE_ID]
|
del s[g.CODEMP_REMOTE_ID]
|
||||||
del s[g.CODEMP_WORKSPACE_ID]
|
del s[g.CODEMP_WORKSPACE_ID]
|
||||||
self.view.erase_status(g.SUBLIME_STATUS_ID)
|
self.view.erase_status(g.SUBLIME_STATUS_ID)
|
||||||
# this does nothing for now. figure out a way later
|
|
||||||
# self.view.erase_regions(g.SUBLIME_REGIONS_PREFIX)
|
tm.stop(f"{g.BUFFCTL_TASK_PREFIX}-{self.codemp_id}")
|
||||||
status_log(f"cleaning up virtual buffer '{self.codemp_id}'")
|
status_log(f"cleaning up virtual buffer '{self.codemp_id}'")
|
||||||
|
|
||||||
|
async def apply_bufferchange_task(self):
|
||||||
|
status_log(f"spinning up '{self.codemp_id}' buffer worker...")
|
||||||
|
try:
|
||||||
|
while text_change := await self.buffctl.recv():
|
||||||
|
if text_change.is_empty():
|
||||||
|
status_log("change is empty. skipping.")
|
||||||
|
continue
|
||||||
|
# In case a change arrives to a background buffer, just apply it.
|
||||||
|
# We are not listening on it. Otherwise, interrupt the listening
|
||||||
|
# to avoid echoing back the change just received.
|
||||||
|
if self.view.id() == g.ACTIVE_CODEMP_VIEW:
|
||||||
|
self.view.settings()[g.CODEMP_IGNORE_NEXT_TEXT_CHANGE] = True
|
||||||
|
|
||||||
|
# we need to go through a sublime text command, since the method,
|
||||||
|
# view.replace needs an edit token, that is obtained only when calling
|
||||||
|
# a textcommand associated with a view.
|
||||||
|
self.view.run_command(
|
||||||
|
"codemp_replace_text",
|
||||||
|
{
|
||||||
|
"start": text_change.start_incl,
|
||||||
|
"end": text_change.end_excl,
|
||||||
|
"content": text_change.content,
|
||||||
|
"change_id": self.view.change_id(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
status_log(f"'{self.codemp_id}' buffer worker stopped...")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
status_log(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)
|
||||||
|
status_log(
|
||||||
|
"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: VirtualWorkspace):
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
|
||||||
# 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, client: VirtualClient, workspace_id: str, handle: Workspace):
|
def __init__(self, workspace_id: str, handle: Workspace):
|
||||||
self.id = workspace_id
|
self.id = workspace_id
|
||||||
self.sublime_window = sublime.active_window()
|
self.sublime_window = sublime.active_window()
|
||||||
self.client = client
|
|
||||||
self.handle = handle
|
self.handle = handle
|
||||||
self.curctl = handle.cursor()
|
self.curctl = handle.cursor()
|
||||||
|
self.isactive = False
|
||||||
|
|
||||||
# mapping remote ids -> local ids
|
# mapping remote ids -> local ids
|
||||||
self.id_map: dict[str, str] = {}
|
self.id_map: dict[str, str] = {}
|
||||||
|
@ -117,11 +173,9 @@ class VirtualWorkspace:
|
||||||
s[g.CODEMP_WINDOW_TAG] = True
|
s[g.CODEMP_WINDOW_TAG] = True
|
||||||
s[g.CODEMP_WINDOW_WORKSPACES] = [self.id]
|
s[g.CODEMP_WINDOW_WORKSPACES] = [self.id]
|
||||||
|
|
||||||
def add_buffer(self, remote_id: str, vbuff: VirtualBuffer):
|
|
||||||
self.id_map[remote_id] = vbuff.view.buffer_id()
|
|
||||||
self.active_buffers[vbuff.view.buffer_id()] = vbuff
|
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
|
self.deactivate()
|
||||||
|
|
||||||
# the worskpace only cares about closing the various open views on its buffers.
|
# the worskpace only cares about closing the various open views on 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.
|
||||||
for vbuff in self.active_buffers.values():
|
for vbuff in self.active_buffers.values():
|
||||||
|
@ -132,7 +186,7 @@ class VirtualWorkspace:
|
||||||
d = self.sublime_window.project_data()
|
d = self.sublime_window.project_data()
|
||||||
newf = list(
|
newf = list(
|
||||||
filter(
|
filter(
|
||||||
lambda F: F["name"] != f"{g.WORKSPACE_FOLDER_PREFIX}{self.id}",
|
lambda f: f.get("name", "") != f"{g.WORKSPACE_FOLDER_PREFIX}{self.id}",
|
||||||
d["folders"],
|
d["folders"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -145,11 +199,38 @@ class VirtualWorkspace:
|
||||||
del s[g.CODEMP_WINDOW_TAG]
|
del s[g.CODEMP_WINDOW_TAG]
|
||||||
del s[g.CODEMP_WINDOW_WORKSPACES]
|
del s[g.CODEMP_WINDOW_WORKSPACES]
|
||||||
|
|
||||||
|
def activate(self):
|
||||||
|
tm.dispatch(
|
||||||
|
self.move_cursor_task(),
|
||||||
|
f"{g.CURCTL_TASK_PREFIX}-{self.id}",
|
||||||
|
)
|
||||||
|
self.isactive = True
|
||||||
|
|
||||||
|
def deactivate(self):
|
||||||
|
if self.isactive:
|
||||||
|
tm.stop(f"{g.CURCTL_TASK_PREFIX}-{self.id}")
|
||||||
|
self.isactive = False
|
||||||
|
|
||||||
|
def add_buffer(self, remote_id: str, vbuff: VirtualBuffer):
|
||||||
|
self.id_map[remote_id] = vbuff.view.buffer_id()
|
||||||
|
self.active_buffers[vbuff.view.buffer_id()] = vbuff
|
||||||
|
|
||||||
def get_by_local(self, local_id: str) -> Optional[VirtualBuffer]:
|
def get_by_local(self, local_id: str) -> Optional[VirtualBuffer]:
|
||||||
return self.active_buffers.get(local_id)
|
return self.active_buffers.get(local_id)
|
||||||
|
|
||||||
def get_by_remote(self, remote_id: str) -> Optional[VirtualBuffer]:
|
def get_by_remote(self, remote_id: str) -> Optional[VirtualBuffer]:
|
||||||
return self.active_buffers.get(self.id_map.get(remote_id))
|
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:
|
||||||
|
status_log(
|
||||||
|
"[WARN] a local-remote buffer id pair was found but not the matching virtual buffer."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
return vbuff
|
||||||
|
|
||||||
async def attach(self, id: str):
|
async def attach(self, id: str):
|
||||||
if id is None:
|
if id is None:
|
||||||
|
@ -170,31 +251,80 @@ class VirtualWorkspace:
|
||||||
status_log(f"error when attaching to buffer '{id}': {e}")
|
status_log(f"error when attaching to buffer '{id}': {e}")
|
||||||
return
|
return
|
||||||
|
|
||||||
view = self.sublime_window.new_file()
|
vbuff = VirtualBuffer(self, id, buff_ctl)
|
||||||
vbuff = VirtualBuffer(self, id, view, buff_ctl)
|
|
||||||
self.add_buffer(id, vbuff)
|
self.add_buffer(id, vbuff)
|
||||||
|
|
||||||
self.client.spawn_buffer_manager(vbuff)
|
|
||||||
|
|
||||||
# TODO! if the view is already active calling focus_view() will not trigger the on_activate
|
# TODO! if the view is already active calling focus_view() will not trigger the on_activate
|
||||||
self.sublime_window.focus_view(view)
|
self.sublime_window.focus_view(vbuff.view)
|
||||||
|
|
||||||
|
async def move_cursor_task(self):
|
||||||
|
status_log(f"spinning up cursor worker for workspace '{self.id}'...")
|
||||||
|
try:
|
||||||
|
while cursor_event := await self.curctl.recv():
|
||||||
|
vbuff = self.get_by_remote(cursor_event.buffer)
|
||||||
|
|
||||||
|
if vbuff is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
reg = rowcol_to_region(vbuff.view, cursor_event.start, cursor_event.end)
|
||||||
|
reg_flags = sublime.RegionFlags.DRAW_EMPTY # show cursors.
|
||||||
|
|
||||||
|
user_hash = hash(cursor_event.user)
|
||||||
|
vbuff.view.add_regions(
|
||||||
|
f"{g.SUBLIME_REGIONS_PREFIX}-{user_hash}",
|
||||||
|
[reg],
|
||||||
|
flags=reg_flags,
|
||||||
|
scope=g.REGIONS_COLORS[user_hash % len(g.REGIONS_COLORS)],
|
||||||
|
annotations=[cursor_event.user],
|
||||||
|
annotation_color=g.PALETTE[user_hash % len(g.PALETTE)],
|
||||||
|
)
|
||||||
|
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
status_log(f"cursor worker for '{self.id}' stopped...")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
status_log(f"cursor worker '{self.id}' crashed:\n{e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
class VirtualClient:
|
class VirtualClient:
|
||||||
def __init__(self, on_exit: Callable = None):
|
def __init__(self):
|
||||||
self.handle: Client = Client()
|
self.handle: Client = Client()
|
||||||
self.workspaces: dict[str, VirtualWorkspace] = {}
|
self.workspaces: dict[str, VirtualWorkspace] = {}
|
||||||
self.active_workspace: VirtualWorkspace = None
|
self.active_workspace: Optional[VirtualWorkspace] = None
|
||||||
self.tm = TaskManager(on_exit)
|
|
||||||
|
|
||||||
def __getitem__(self, key: str):
|
def __getitem__(self, key: str):
|
||||||
return self.workspaces.get(key)
|
return self.workspaces.get(key)
|
||||||
|
|
||||||
def make_active(self, ws: VirtualWorkspace):
|
def get_workspace(self, view):
|
||||||
|
tag_id = view.settings().get(g.CODEMP_WORKSPACE_ID)
|
||||||
|
if tag_id is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
ws = self.workspaces.get(tag_id)
|
||||||
|
if ws is None:
|
||||||
|
status_log(
|
||||||
|
"[WARN] a tag on the view was found but not a matching workspace."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
return ws
|
||||||
|
|
||||||
|
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: VirtualWorkspace | None):
|
||||||
|
if self.active_workspace == ws:
|
||||||
|
return
|
||||||
|
|
||||||
if self.active_workspace is not None:
|
if self.active_workspace is not None:
|
||||||
self.tm.stop(f"{g.CURCTL_TASK_PREFIX}-{self.active_workspace.id}")
|
self.active_workspace.deactivate()
|
||||||
|
|
||||||
|
if ws is not None:
|
||||||
|
ws.activate()
|
||||||
|
|
||||||
self.active_workspace = ws
|
self.active_workspace = ws
|
||||||
self.spawn_cursor_manager(ws)
|
|
||||||
|
|
||||||
async def connect(self, server_host: str):
|
async def connect(self, server_host: str):
|
||||||
status_log(f"Connecting to {server_host}")
|
status_log(f"Connecting to {server_host}")
|
||||||
|
@ -211,12 +341,14 @@ class VirtualClient:
|
||||||
|
|
||||||
async def join_workspace(
|
async def join_workspace(
|
||||||
self, workspace_id: str, user="sublime2", password="lmaodefaultpassword"
|
self, workspace_id: str, user="sublime2", password="lmaodefaultpassword"
|
||||||
) -> VirtualWorkspace:
|
) -> Optional[VirtualWorkspace]:
|
||||||
try:
|
try:
|
||||||
status_log(f"Logging into workspace: '{workspace_id}'")
|
status_log(f"Logging into workspace: '{workspace_id}'")
|
||||||
await self.handle.login(user, password, workspace_id)
|
await self.handle.login(user, password, workspace_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
status_log(f"Failed to login to workspace '{workspace_id}'.\nerror: {e}", True)
|
status_log(
|
||||||
|
f"Failed to login to workspace '{workspace_id}'.\nerror: {e}", True
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -226,105 +358,11 @@ class VirtualClient:
|
||||||
status_log(f"Could not join workspace '{workspace_id}'.\nerror: {e}", True)
|
status_log(f"Could not join workspace '{workspace_id}'.\nerror: {e}", True)
|
||||||
return
|
return
|
||||||
|
|
||||||
vws = VirtualWorkspace(self, workspace_id, workspace_handle)
|
vws = VirtualWorkspace(workspace_id, workspace_handle)
|
||||||
self.make_active(vws)
|
|
||||||
self.workspaces[workspace_id] = vws
|
self.workspaces[workspace_id] = vws
|
||||||
|
self.make_active(vws)
|
||||||
|
|
||||||
return vws
|
return vws
|
||||||
|
|
||||||
def spawn_cursor_manager(self, virtual_workspace: VirtualWorkspace):
|
|
||||||
async def move_cursor_task(vws):
|
|
||||||
status_log(f"spinning up cursor worker for workspace '{vws.id}'...")
|
|
||||||
try:
|
|
||||||
while cursor_event := await vws.curctl.recv():
|
|
||||||
vbuff = vws.get_by_remote(cursor_event.buffer)
|
|
||||||
|
|
||||||
if vbuff is None:
|
client = VirtualClient()
|
||||||
continue
|
|
||||||
|
|
||||||
reg = rowcol_to_region(
|
|
||||||
vbuff.view, cursor_event.start, cursor_event.end
|
|
||||||
)
|
|
||||||
reg_flags = sublime.RegionFlags.DRAW_EMPTY # show cursors.
|
|
||||||
|
|
||||||
user_hash = hash(cursor_event.user)
|
|
||||||
vbuff.view.add_regions(
|
|
||||||
f"{g.SUBLIME_REGIONS_PREFIX}-{user_hash}",
|
|
||||||
[reg],
|
|
||||||
flags=reg_flags,
|
|
||||||
scope=g.REGIONS_COLORS[user_hash % len(g.REGIONS_COLORS)],
|
|
||||||
annotations=[cursor_event.user],
|
|
||||||
annotation_color=g.PALETTE[user_hash % len(g.PALETTE)],
|
|
||||||
)
|
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
status_log(f"cursor worker for '{vws.id}' stopped...")
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
status_log(f"cursor worker '{vws.id}' crashed:\n{e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
self.tm.dispatch(
|
|
||||||
move_cursor_task(virtual_workspace),
|
|
||||||
f"{g.CURCTL_TASK_PREFIX}-{virtual_workspace.id}",
|
|
||||||
)
|
|
||||||
|
|
||||||
def send_cursor(self, vbuff: VirtualBuffer):
|
|
||||||
# TODO: only the last placed cursor/selection.
|
|
||||||
# status_log(f"sending cursor position in workspace: {vbuff.workspace.id}")
|
|
||||||
region = vbuff.view.sel()[0]
|
|
||||||
start = vbuff.view.rowcol(region.begin()) # only counts UTF8 chars
|
|
||||||
end = vbuff.view.rowcol(region.end())
|
|
||||||
|
|
||||||
vbuff.workspace.curctl.send(vbuff.codemp_id, start, end)
|
|
||||||
|
|
||||||
def spawn_buffer_manager(self, vbuff: VirtualBuffer):
|
|
||||||
async def apply_buffer_change_task(vb):
|
|
||||||
status_log(f"spinning up '{vb.codemp_id}' buffer worker...")
|
|
||||||
try:
|
|
||||||
while text_change := await vb.buffctl.recv():
|
|
||||||
if text_change.is_empty():
|
|
||||||
status_log("change is empty. skipping.")
|
|
||||||
continue
|
|
||||||
# In case a change arrives to a background buffer, just apply it.
|
|
||||||
# We are not listening on it. Otherwise, interrupt the listening
|
|
||||||
# to avoid echoing back the change just received.
|
|
||||||
if vb.view.id() == g.ACTIVE_CODEMP_VIEW:
|
|
||||||
vb.view.settings()[g.CODEMP_IGNORE_NEXT_TEXT_CHANGE] = True
|
|
||||||
|
|
||||||
# we need to go through a sublime text command, since the method,
|
|
||||||
# view.replace needs an edit token, that is obtained only when calling
|
|
||||||
# a textcommand associated with a view.
|
|
||||||
vb.view.run_command(
|
|
||||||
"codemp_replace_text",
|
|
||||||
{
|
|
||||||
"start": text_change.start_incl,
|
|
||||||
"end": text_change.end_excl,
|
|
||||||
"content": text_change.content,
|
|
||||||
"change_id": vb.view.change_id(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
status_log(f"'{vb.codemp_id}' buffer worker stopped...")
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
status_log(f"buffer worker '{vb.codemp_id}' crashed:\n{e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
self.tm.dispatch(
|
|
||||||
apply_buffer_change_task(vbuff),
|
|
||||||
f"{g.BUFFCTL_TASK_PREFIX}-{vbuff.codemp_id}",
|
|
||||||
)
|
|
||||||
|
|
||||||
def send_buffer_change(self, changes, vbuff: VirtualBuffer):
|
|
||||||
# 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)
|
|
||||||
status_log(
|
|
||||||
"sending txt change: Reg({} {}) -> '{}'".format(
|
|
||||||
region.begin(), region.end(), change.str
|
|
||||||
)
|
|
||||||
)
|
|
||||||
vbuff.buffctl.send(region.begin(), region.end(), change.str)
|
|
|
@ -12,7 +12,9 @@ WORKSPACE_FOLDER_PREFIX = "CODEMP::"
|
||||||
SUBLIME_REGIONS_PREFIX = "codemp-cursors"
|
SUBLIME_REGIONS_PREFIX = "codemp-cursors"
|
||||||
SUBLIME_STATUS_ID = "z_codemp_buffer"
|
SUBLIME_STATUS_ID = "z_codemp_buffer"
|
||||||
CODEMP_IGNORE_NEXT_TEXT_CHANGE = "codemp-skip-change-event"
|
CODEMP_IGNORE_NEXT_TEXT_CHANGE = "codemp-skip-change-event"
|
||||||
|
|
||||||
ACTIVE_CODEMP_VIEW = None
|
ACTIVE_CODEMP_VIEW = None
|
||||||
|
ACTIVE_CODEMP_WINDOW = None
|
||||||
|
|
||||||
PALETTE = [
|
PALETTE = [
|
||||||
"var(--redish)",
|
"var(--redish)",
|
||||||
|
|
|
@ -16,10 +16,15 @@ def rowcol_to_region(view, start, end):
|
||||||
|
|
||||||
|
|
||||||
def safe_listener_detach(txt_listener: sublime_plugin.TextChangeListener):
|
def safe_listener_detach(txt_listener: sublime_plugin.TextChangeListener):
|
||||||
if txt_listener.is_attached():
|
if txt_listener is not None and txt_listener.is_attached():
|
||||||
txt_listener.detach()
|
txt_listener.detach()
|
||||||
|
|
||||||
|
|
||||||
|
def safe_listener_attach(txt_listener: sublime_plugin.TextChangeListener, buffer):
|
||||||
|
if txt_listener is not None and not txt_listener.is_attached():
|
||||||
|
txt_listener.attach(buffer)
|
||||||
|
|
||||||
|
|
||||||
def get_contents(view):
|
def get_contents(view):
|
||||||
r = sublime.Region(0, view.size())
|
r = sublime.Region(0, view.size())
|
||||||
return view.substr(r)
|
return view.substr(r)
|
||||||
|
|
Loading…
Reference in a new issue