mirror of
https://github.com/hexedtech/codemp-sublime.git
synced 2024-12-23 05:04:52 +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
c29c95fe55
commit
744887b336
5 changed files with 260 additions and 198 deletions
111
plugin.py
111
plugin.py
|
@ -5,71 +5,80 @@ import sublime_plugin
|
|||
# import sys
|
||||
# import importlib.util
|
||||
|
||||
from .src.codemp_client import VirtualClient
|
||||
from .src.codemp_client import CodempLogger
|
||||
from .src.TaskManager import rt
|
||||
from .src.TaskManager import tm
|
||||
from .src.client import client, VirtualClient
|
||||
from .src.client import CodempLogger
|
||||
from .src.utils import status_log
|
||||
from .src.utils import safe_listener_detach
|
||||
from .src.utils import safe_listener_attach
|
||||
from .src import globals as g
|
||||
from .bindings.codemp import init_logger
|
||||
|
||||
CLIENT = None
|
||||
TEXT_LISTENER = None
|
||||
|
||||
|
||||
# Initialisation and Deinitialisation
|
||||
##############################################################################
|
||||
|
||||
|
||||
def plugin_loaded():
|
||||
global CLIENT
|
||||
global TEXT_LISTENER
|
||||
|
||||
# instantiate and start a global asyncio event loop.
|
||||
# pass in the exit_handler coroutine that will be called upon relasing the event loop.
|
||||
CLIENT = VirtualClient(disconnect_client)
|
||||
TEXT_LISTENER = CodempClientTextChangeListener()
|
||||
tm.acquire(disconnect_client)
|
||||
|
||||
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")
|
||||
|
||||
|
||||
async def disconnect_client():
|
||||
global CLIENT
|
||||
global TEXT_LISTENER
|
||||
|
||||
safe_listener_detach(TEXT_LISTENER)
|
||||
CLIENT.tm.stop_all()
|
||||
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()
|
||||
|
||||
|
||||
def plugin_unloaded():
|
||||
global CLIENT
|
||||
# releasing the runtime, runs the disconnect callback defined when acquiring the event loop.
|
||||
CLIENT.tm.release(False)
|
||||
status_log("unloading")
|
||||
tm.release(False)
|
||||
|
||||
|
||||
# Listeners
|
||||
##############################################################################
|
||||
class EventListener(sublime_plugin.EventListener):
|
||||
def on_exit(self):
|
||||
global CLIENT
|
||||
CLIENT.tm.release(True)
|
||||
tm.release(True)
|
||||
|
||||
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()
|
||||
if s.get(g.CODEMP_WINDOW_TAG, False):
|
||||
if not s.get(g.CODEMP_WINDOW_TAG, False):
|
||||
return
|
||||
|
||||
for wsid in s[g.CODEMP_WINDOW_WORKSPACES]:
|
||||
ws = CLIENT[wsid]
|
||||
if ws is not None:
|
||||
if ws.id == CLIENT.active_workspace.id:
|
||||
CLIENT.active_workspace = None
|
||||
CLIENT.tm.stop(f"{g.CURCTL_TASK_PREFIX}-{ws.id}")
|
||||
ws = client[wsid]
|
||||
if ws is None:
|
||||
status_log(
|
||||
"[WARN] a tag on the window was found but not a matching workspace."
|
||||
)
|
||||
continue
|
||||
|
||||
ws.cleanup()
|
||||
del CLIENT.workspaces[wsid]
|
||||
del client.workspaces[wsid]
|
||||
|
||||
|
||||
class CodempClientViewEventListener(sublime_plugin.ViewEventListener):
|
||||
|
@ -82,12 +91,13 @@ class CodempClientViewEventListener(sublime_plugin.ViewEventListener):
|
|||
return False
|
||||
|
||||
def on_selection_modified_async(self):
|
||||
s = self.view.settings()
|
||||
ws = client.get_workspace(self.view)
|
||||
if ws is None:
|
||||
return
|
||||
|
||||
global CLIENT
|
||||
vbuff = CLIENT[s[g.CODEMP_WORKSPACE_ID]].get_by_local(self.view.buffer_id())
|
||||
vbuff = ws.get_by_local(self.view.buffer_id())
|
||||
if vbuff is not None:
|
||||
CLIENT.send_cursor(vbuff)
|
||||
vbuff.send_cursor(ws)
|
||||
|
||||
def on_activated(self):
|
||||
# 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()
|
||||
# print("view {} activated".format(self.view.id()))
|
||||
global TEXT_LISTENER
|
||||
TEXT_LISTENER.attach(self.view.buffer())
|
||||
safe_listener_attach(TEXT_LISTENER, self.view.buffer())
|
||||
|
||||
def on_deactivated(self):
|
||||
g.ACTIVE_CODEMP_VIEW = None
|
||||
|
@ -108,12 +118,13 @@ class CodempClientViewEventListener(sublime_plugin.ViewEventListener):
|
|||
if self.view.id() == g.ACTIVE_CODEMP_VIEW:
|
||||
safe_listener_detach(TEXT_LISTENER)
|
||||
|
||||
global CLIENT
|
||||
wsid = self.view.settings().get(g.CODEMP_WORKSPACE_ID)
|
||||
vbuff = CLIENT[wsid].get_by_local(self.view.buffer_id())
|
||||
vbuff.cleanup()
|
||||
ws = client.get_workspace(self.view)
|
||||
if ws is None:
|
||||
return
|
||||
|
||||
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):
|
||||
|
@ -131,9 +142,9 @@ class CodempClientTextChangeListener(sublime_plugin.TextChangeListener):
|
|||
s[g.CODEMP_IGNORE_NEXT_TEXT_CHANGE] = False
|
||||
return
|
||||
|
||||
global CLIENT
|
||||
vbuff = CLIENT[s[g.CODEMP_WORKSPACE_ID]].get_by_local(self.buffer.id())
|
||||
CLIENT.send_buffer_change(changes, vbuff)
|
||||
vbuff = client.get_buffer(self.buffer.primary_view())
|
||||
if vbuff is not None:
|
||||
vbuff.send_buffer_change(changes)
|
||||
|
||||
|
||||
# Commands:
|
||||
|
@ -153,8 +164,7 @@ class CodempClientTextChangeListener(sublime_plugin.TextChangeListener):
|
|||
#############################################################################
|
||||
class CodempConnectCommand(sublime_plugin.WindowCommand):
|
||||
def run(self, server_host):
|
||||
global CLIENT
|
||||
rt.dispatch(CLIENT.connect(server_host))
|
||||
tm.dispatch(client.connect(server_host))
|
||||
|
||||
def input(self, 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):
|
||||
def run(self, workspace_id, buffer_id):
|
||||
global CLIENT
|
||||
rt.dispatch(JoinCommand(CLIENT, workspace_id, buffer_id))
|
||||
tm.dispatch(JoinCommand(client, workspace_id, buffer_id))
|
||||
|
||||
def input_description(self):
|
||||
return "Join:"
|
||||
|
@ -189,8 +198,7 @@ class CodempJoinCommand(sublime_plugin.WindowCommand):
|
|||
#############################################################################
|
||||
class CodempJoinWorkspaceCommand(sublime_plugin.WindowCommand):
|
||||
def run(self, workspace_id):
|
||||
global CLIENT
|
||||
rt.dispatch(CLIENT.join_workspace(workspace_id, "sublime3"))
|
||||
tm.dispatch(client.join_workspace(workspace_id, "sublime3"))
|
||||
|
||||
def input_description(self):
|
||||
return "Join specific workspace"
|
||||
|
@ -204,23 +212,21 @@ class CodempJoinWorkspaceCommand(sublime_plugin.WindowCommand):
|
|||
#############################################################################
|
||||
class CodempJoinBufferCommand(sublime_plugin.WindowCommand):
|
||||
def run(self, buffer_id):
|
||||
global CLIENT
|
||||
if CLIENT.active_workspace is None:
|
||||
if client.active_workspace is None:
|
||||
sublime.error_message(
|
||||
"You haven't joined any worksapce yet. \
|
||||
use `Codemp: Join Workspace` or `Codemp: Join`"
|
||||
)
|
||||
return
|
||||
|
||||
rt.dispatch(CLIENT.active_workspace.attach(buffer_id))
|
||||
tm.dispatch(client.active_workspace.attach(buffer_id))
|
||||
|
||||
def input_description(self):
|
||||
return "Join buffer in the active workspace"
|
||||
|
||||
# This is awful, fix it
|
||||
def input(self, args):
|
||||
global CLIENT
|
||||
if CLIENT.active_workspace is None:
|
||||
if client.active_workspace is None:
|
||||
sublime.error_message(
|
||||
"You haven't joined any worksapce yet. \
|
||||
use `Codemp: Join Workspace` or `Codemp: Join`"
|
||||
|
@ -228,7 +234,7 @@ class CodempJoinBufferCommand(sublime_plugin.WindowCommand):
|
|||
return
|
||||
|
||||
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:
|
||||
return RawBufferId()
|
||||
else:
|
||||
|
@ -259,8 +265,7 @@ class ListBufferId(sublime_plugin.ListInputHandler):
|
|||
return "buffer_id"
|
||||
|
||||
def list_items(self):
|
||||
global CLIENT
|
||||
return CLIENT.active_workspace.handle.filetree()
|
||||
return client.active_workspace.handle.filetree()
|
||||
|
||||
def next_input(self, args):
|
||||
if "buffer_id" not in args:
|
||||
|
@ -313,7 +318,7 @@ class RawBufferId(sublime_plugin.TextInputHandler):
|
|||
#############################################################################
|
||||
class CodempDisconnectCommand(sublime_plugin.WindowCommand):
|
||||
def run(self):
|
||||
rt.sync(disconnect_client())
|
||||
tm.sync(disconnect_client())
|
||||
|
||||
|
||||
# Proxy Commands ( NOT USED, left just in case we need it again. )
|
||||
|
|
|
@ -1,21 +1,29 @@
|
|||
from typing import Optional
|
||||
import asyncio
|
||||
import Codemp.ext.sublime_asyncio as rt
|
||||
from ..ext import sublime_asyncio as rt
|
||||
|
||||
|
||||
class TaskManager:
|
||||
def __init__(self, exit_handler):
|
||||
def __init__(self):
|
||||
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):
|
||||
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):
|
||||
rt.dispatch(coro, self.store_named_lambda(name))
|
||||
def dispatch(self, coro, name=None):
|
||||
self.runtime.dispatch(coro, self.store_named_lambda(name))
|
||||
|
||||
def sync(self, coro):
|
||||
rt.sync(coro)
|
||||
self.runtime.sync(coro)
|
||||
|
||||
def remove_stopped(self):
|
||||
self.tasks = list(filter(lambda T: not T.cancelled(), self.tasks))
|
||||
|
@ -26,7 +34,7 @@ class TaskManager:
|
|||
self.tasks.append(task)
|
||||
self.remove_stopped()
|
||||
|
||||
def store_named_lambda(self, name):
|
||||
def store_named_lambda(self, name=None):
|
||||
def _store(task):
|
||||
self.store(task, name)
|
||||
|
||||
|
@ -43,7 +51,7 @@ class TaskManager:
|
|||
def pop_task(self, name) -> Optional:
|
||||
idx = self.get_task_idx(name)
|
||||
if id is not None:
|
||||
return self.task.pop(idx)
|
||||
return self.tasks.pop(idx)
|
||||
return None
|
||||
|
||||
async def _stop(self, task):
|
||||
|
@ -56,8 +64,12 @@ class TaskManager:
|
|||
def stop(self, name):
|
||||
t = self.get_task(name)
|
||||
if t is not None:
|
||||
rt.dispatch(self._stop(t))
|
||||
self.runtime.dispatch(self._stop(t))
|
||||
|
||||
def stop_all(self):
|
||||
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 typing import Optional, Callable
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import sublime
|
||||
import asyncio
|
||||
|
@ -8,11 +7,10 @@ import tempfile
|
|||
import os
|
||||
import shutil
|
||||
|
||||
|
||||
import Codemp.src.globals as g
|
||||
from Codemp.src.wrappers import BufferController, Workspace, Client
|
||||
from Codemp.src.utils import status_log, rowcol_to_region
|
||||
from Codemp.src.TaskManager import TaskManager
|
||||
from ..src import globals as g
|
||||
from ..src.TaskManager import tm
|
||||
from ..src.wrappers import BufferController, Workspace, Client
|
||||
from ..src.utils import status_log, rowcol_to_region
|
||||
|
||||
|
||||
class CodempLogger:
|
||||
|
@ -22,7 +20,7 @@ class CodempLogger:
|
|||
async def message(self):
|
||||
return await self.handle.message()
|
||||
|
||||
async def spawn_logger(self):
|
||||
async def log(self):
|
||||
status_log("spinning up the logger...")
|
||||
try:
|
||||
while msg := await self.handle.message():
|
||||
|
@ -44,12 +42,11 @@ class VirtualBuffer:
|
|||
self,
|
||||
workspace: VirtualWorkspace,
|
||||
remote_id: str,
|
||||
view: sublime.View,
|
||||
buffctl: BufferController,
|
||||
):
|
||||
self.view = view
|
||||
self.view = sublime.active_window().new_file()
|
||||
self.codemp_id = remote_id
|
||||
self.sublime_id = view.buffer_id()
|
||||
self.sublime_id = self.view.buffer_id()
|
||||
|
||||
self.workspace = workspace
|
||||
self.buffctl = buffctl
|
||||
|
@ -61,6 +58,11 @@ class VirtualBuffer:
|
|||
self.view.retarget(self.tmpfile)
|
||||
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
|
||||
s = self.view.settings()
|
||||
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_WORKSPACE_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}'")
|
||||
|
||||
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
|
||||
# events that happen to the codemp workspaces into sublime actions
|
||||
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.sublime_window = sublime.active_window()
|
||||
self.client = client
|
||||
self.handle = handle
|
||||
self.curctl = handle.cursor()
|
||||
self.isactive = False
|
||||
|
||||
# mapping remote ids -> local ids
|
||||
self.id_map: dict[str, str] = {}
|
||||
|
@ -117,11 +173,9 @@ class VirtualWorkspace:
|
|||
s[g.CODEMP_WINDOW_TAG] = True
|
||||
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):
|
||||
self.deactivate()
|
||||
|
||||
# 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.
|
||||
for vbuff in self.active_buffers.values():
|
||||
|
@ -132,7 +186,7 @@ class VirtualWorkspace:
|
|||
d = self.sublime_window.project_data()
|
||||
newf = list(
|
||||
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"],
|
||||
)
|
||||
)
|
||||
|
@ -145,11 +199,38 @@ class VirtualWorkspace:
|
|||
del s[g.CODEMP_WINDOW_TAG]
|
||||
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]:
|
||||
return self.active_buffers.get(local_id)
|
||||
|
||||
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):
|
||||
if id is None:
|
||||
|
@ -170,31 +251,80 @@ class VirtualWorkspace:
|
|||
status_log(f"error when attaching to buffer '{id}': {e}")
|
||||
return
|
||||
|
||||
view = self.sublime_window.new_file()
|
||||
vbuff = VirtualBuffer(self, id, view, buff_ctl)
|
||||
vbuff = VirtualBuffer(self, id, buff_ctl)
|
||||
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
|
||||
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:
|
||||
def __init__(self, on_exit: Callable = None):
|
||||
def __init__(self):
|
||||
self.handle: Client = Client()
|
||||
self.workspaces: dict[str, VirtualWorkspace] = {}
|
||||
self.active_workspace: VirtualWorkspace = None
|
||||
self.tm = TaskManager(on_exit)
|
||||
self.active_workspace: Optional[VirtualWorkspace] = None
|
||||
|
||||
def __getitem__(self, key: str):
|
||||
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:
|
||||
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.spawn_cursor_manager(ws)
|
||||
|
||||
async def connect(self, server_host: str):
|
||||
status_log(f"Connecting to {server_host}")
|
||||
|
@ -211,12 +341,14 @@ class VirtualClient:
|
|||
|
||||
async def join_workspace(
|
||||
self, workspace_id: str, user="sublime2", password="***REMOVED***"
|
||||
) -> VirtualWorkspace:
|
||||
) -> Optional[VirtualWorkspace]:
|
||||
try:
|
||||
status_log(f"Logging into workspace: '{workspace_id}'")
|
||||
await self.handle.login(user, password, workspace_id)
|
||||
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
|
||||
|
||||
try:
|
||||
|
@ -226,105 +358,11 @@ class VirtualClient:
|
|||
status_log(f"Could not join workspace '{workspace_id}'.\nerror: {e}", True)
|
||||
return
|
||||
|
||||
vws = VirtualWorkspace(self, workspace_id, workspace_handle)
|
||||
self.make_active(vws)
|
||||
vws = VirtualWorkspace(workspace_id, workspace_handle)
|
||||
self.workspaces[workspace_id] = vws
|
||||
self.make_active(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:
|
||||
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)
|
||||
client = VirtualClient()
|
|
@ -12,7 +12,9 @@ WORKSPACE_FOLDER_PREFIX = "CODEMP::"
|
|||
SUBLIME_REGIONS_PREFIX = "codemp-cursors"
|
||||
SUBLIME_STATUS_ID = "z_codemp_buffer"
|
||||
CODEMP_IGNORE_NEXT_TEXT_CHANGE = "codemp-skip-change-event"
|
||||
|
||||
ACTIVE_CODEMP_VIEW = None
|
||||
ACTIVE_CODEMP_WINDOW = None
|
||||
|
||||
PALETTE = [
|
||||
"var(--redish)",
|
||||
|
|
|
@ -16,10 +16,15 @@ def rowcol_to_region(view, start, end):
|
|||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
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):
|
||||
r = sublime.Region(0, view.size())
|
||||
return view.substr(r)
|
||||
|
|
Loading…
Reference in a new issue