mirror of
https://github.com/hexedtech/codemp-sublime.git
synced 2024-11-22 06:44:48 +01:00
Added callback logic to buffer event. Fixed double send. Improved docs a bit.
Former-commit-id: cd4407bad42657f0886b59fd816e2c9f396215a9
This commit is contained in:
parent
fd528237b3
commit
029be23fe7
2 changed files with 128 additions and 63 deletions
185
plugin.py
185
plugin.py
|
@ -12,7 +12,8 @@ _tasks = []
|
||||||
_client = None
|
_client = None
|
||||||
_cursor_controller = None
|
_cursor_controller = None
|
||||||
_buffer_controller = None
|
_buffer_controller = None
|
||||||
_setting_key = "codemp_buffer"
|
_txt_change_listener = None
|
||||||
|
_skip_resend = False
|
||||||
|
|
||||||
_regions_colors = [
|
_regions_colors = [
|
||||||
"region.redish",
|
"region.redish",
|
||||||
|
@ -58,13 +59,30 @@ def plugin_loaded():
|
||||||
sublime_asyncio.acquire() # instantiate and start a global asyncio event loop.
|
sublime_asyncio.acquire() # instantiate and start a global asyncio event loop.
|
||||||
|
|
||||||
def plugin_unloaded():
|
def plugin_unloaded():
|
||||||
|
global _client
|
||||||
|
global _cursor_controller
|
||||||
|
global _buffer_controller
|
||||||
|
global _txt_change_listener
|
||||||
for window in sublime.windows():
|
for window in sublime.windows():
|
||||||
for view in window.views():
|
for view in window.views():
|
||||||
if "codemp_buffer" in view.settings():
|
if view.settings().get("codemp_buffer", False):
|
||||||
|
sublime_asyncio.dispatch(_client.disconnect_buffer(view.file_name()))
|
||||||
del view.settings()["codemp_buffer"]
|
del view.settings()["codemp_buffer"]
|
||||||
# disconnect all buffers
|
view.erase_regions("codemp_cursors")
|
||||||
# stop all callbacks
|
|
||||||
|
if _cursor_controller:
|
||||||
|
_cursor_controller.drop_callback()
|
||||||
|
|
||||||
|
if _buffer_controller:
|
||||||
|
_buffer_controller.drop_callback()
|
||||||
|
|
||||||
|
if _txt_change_listener:
|
||||||
|
if _txt_change_listener.is_attached():
|
||||||
|
_txt_change_listener.detach()
|
||||||
|
_txt_change_listener = None
|
||||||
|
|
||||||
# disconnect the client.
|
# disconnect the client.
|
||||||
|
sublime_asyncio.dispatch(_client.leave_workspace())
|
||||||
print("unloading")
|
print("unloading")
|
||||||
|
|
||||||
async def connect_command(server_host, session="default"):
|
async def connect_command(server_host, session="default"):
|
||||||
|
@ -85,6 +103,7 @@ async def share_buffer_command(buffer):
|
||||||
global _client
|
global _client
|
||||||
global _cursor_controller
|
global _cursor_controller
|
||||||
global _buffer_controller
|
global _buffer_controller
|
||||||
|
global _txt_change_listener
|
||||||
|
|
||||||
status_log("Sharing buffer {}".format(buffer))
|
status_log("Sharing buffer {}".format(buffer))
|
||||||
|
|
||||||
|
@ -95,12 +114,16 @@ async def share_buffer_command(buffer):
|
||||||
await _client.create(buffer, contents)
|
await _client.create(buffer, contents)
|
||||||
|
|
||||||
_buffer_controller = await _client.attach(buffer)
|
_buffer_controller = await _client.attach(buffer)
|
||||||
_buffer_controller.callback(apply_buffer_change)
|
# apply_buffer_change returns a "lambda" that internalises the buffer it is being called on.
|
||||||
|
_buffer_controller.callback(apply_buffer_change(view.buffer()))
|
||||||
|
|
||||||
|
# we create a new event listener and attach it to the shared buffer.
|
||||||
|
_txt_change_listener = CodempClientTextChangeListener()
|
||||||
|
_txt_change_listener.attach(view.buffer())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
sublime.error_message("Could not share buffer: {}".format(e))
|
sublime.error_message("Could not share buffer: {}".format(e))
|
||||||
return
|
return
|
||||||
|
|
||||||
status_log("Listening")
|
|
||||||
view.set_status("z_codemp_buffer", "[Codemp]")
|
view.set_status("z_codemp_buffer", "[Codemp]")
|
||||||
view.settings()["codemp_buffer"] = True
|
view.settings()["codemp_buffer"] = True
|
||||||
|
|
||||||
|
@ -110,11 +133,17 @@ def move_cursor(cursor_event):
|
||||||
# TODO: make the matching user/color more solid. now all users have one color cursor.
|
# TODO: make the matching user/color more solid. now all users have one color cursor.
|
||||||
# Maybe make all cursors the same color and only use annotations as a discriminant.
|
# Maybe make all cursors the same color and only use annotations as a discriminant.
|
||||||
view = get_matching_view(cursor_event.buffer)
|
view = get_matching_view(cursor_event.buffer)
|
||||||
if "codemp_buffer" in view.settings():
|
if view.settings().get("codemp_buffer", False):
|
||||||
reg = rowcol_to_region(view, cursor_event.start, cursor_event.end)
|
reg = rowcol_to_region(view, cursor_event.start, cursor_event.end)
|
||||||
reg_flags = sublime.RegionFlags.DRAW_EMPTY | sublime.RegionFlags.DRAW_NO_FILL
|
reg_flags = sublime.RegionFlags.DRAW_EMPTY | sublime.RegionFlags.DRAW_NO_FILL
|
||||||
|
|
||||||
view.add_regions("codemp_cursors", [reg], flags = reg_flags, scope=_regions_colors[2], annotations = [cursor_event.user])
|
view.add_regions(
|
||||||
|
"codemp_cursors",
|
||||||
|
[reg],
|
||||||
|
flags = reg_flags,
|
||||||
|
scope=_regions_colors[2],
|
||||||
|
annotations = [cursor_event.user],
|
||||||
|
annotation_color="#000")
|
||||||
|
|
||||||
def send_cursor(view):
|
def send_cursor(view):
|
||||||
global _cursor_controller
|
global _cursor_controller
|
||||||
|
@ -132,26 +161,31 @@ def send_buffer_change(buffer, changes):
|
||||||
view = buffer.primary_view()
|
view = buffer.primary_view()
|
||||||
start, txt, end = compress_changes(view, changes)
|
start, txt, end = compress_changes(view, changes)
|
||||||
|
|
||||||
|
# we can't use view.size() since now view has the already modified buffer,
|
||||||
|
# but we need to clip wrt the unmodified buffer.
|
||||||
contlen = len(_buffer_controller.get_content())
|
contlen = len(_buffer_controller.get_content())
|
||||||
_buffer_controller.delta(start, txt, min(end, contlen))
|
_buffer_controller.delta(max(0, start), txt, min(end, contlen))
|
||||||
|
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
print("server buffer: -------")
|
print("server buffer: -------")
|
||||||
print(_buffer_controller.get_content())
|
print(_buffer_controller.get_content())
|
||||||
|
|
||||||
def compress_changes(view, changes):
|
def compress_changes(view, changes):
|
||||||
## TODO: doesn't work correctly.
|
|
||||||
|
|
||||||
# Sublime text on_text_changed events, gives a list of changes.
|
# Sublime text on_text_changed events, gives a list of changes.
|
||||||
# in case of simple insertion or deletion this is fine.
|
# in case of simple insertion or deletion this is fine.
|
||||||
# but if we swap a string (select it and add another string in it's place) or have multiple selections
|
# but if we swap a string (select it and add another string in it's place) or have multiple selections
|
||||||
# we receive two split text changes, first we add the new string in front of the selection
|
# or do an undo of some kind after the just mentioned events we receive multiple split text changes,
|
||||||
# and then we delete the old selection. e.g: [1234] -> hello is split into: [1234] -> hello[1234] -> hello[]
|
# e.g. select the world `hello` and replace it with `12345`: Sublime will separate it into two singular changes,
|
||||||
# this fucks over the operations factory algorithm, which panics if reading the operations sequentially,
|
# first add `12345` in front of `hello`: `12345hello` then, delete the `hello`.
|
||||||
# since the changes refer to the same point in time and are not updated each time.
|
# The gotcha here is that now we have an issue of indexing inside the buffer. when adding `12345` we shifted the index of the
|
||||||
|
# start of the word `hello` to the right by 5.
|
||||||
|
# By sending these changes one by one generated some buffer length issues in delta, since we have an interdependency of the
|
||||||
|
# changes.
|
||||||
|
|
||||||
# as a workaround, we compress all changes into a big change, which gives the region in which the change occurred
|
# as a workaround, whenever we receive multiple changes we compress all of them into a "single one" that delta understands,
|
||||||
# and the new string, extracted directly from the local buffer already modified.
|
# namely, we get a bounding region to the change, and all the text in between.
|
||||||
if len(changes) == 1:
|
if len(changes) == 1:
|
||||||
|
print("[change]", "[", changes[0].a.pt, changes[0].b.pt, "]", changes[0].str)
|
||||||
return (changes[0].a.pt, changes[0].str, changes[0].b.pt)
|
return (changes[0].a.pt, changes[0].str, changes[0].b.pt)
|
||||||
|
|
||||||
return walk_compress_changes(view, changes)
|
return walk_compress_changes(view, changes)
|
||||||
|
@ -175,20 +209,24 @@ def walk_compress_changes(view, changes):
|
||||||
# .len_utf8 is the length of the deleted/canceled string in the buffer
|
# .len_utf8 is the length of the deleted/canceled string in the buffer
|
||||||
change_delta = len(change.str) - change.len_utf8
|
change_delta = len(change.str) - change.len_utf8
|
||||||
|
|
||||||
txt_a = min(txt_a, change.a.pt) # the text region is enlarged to the left
|
# the text region is enlarged to the left
|
||||||
|
txt_a = min(txt_a, change.a.pt)
|
||||||
|
|
||||||
# On insertion, change.b.pt == change.a.pt
|
# On insertion, change.b.pt == change.a.pt
|
||||||
# If we meet a new insertion further than the current window
|
# If we meet a new insertion further than the current window
|
||||||
# we expand to the right by that change.
|
# we expand to the right by that change.
|
||||||
# On deletion, change.a.pt == change.b.pt - change.len_utf8
|
# On deletion, change.a.pt == change.b.pt - change.len_utf8
|
||||||
# when we delete a selection and it is further than the current window
|
# when we delete a selection and it is further than the current window
|
||||||
# we enlarge to the right up until the begin of the deleted region.
|
# we enlarge to the right up until the begin of the deleted region.
|
||||||
if change.b.pt > txt_b:
|
if change.b.pt > txt_b:
|
||||||
txt_b = change.b.pt + change_delta
|
txt_b = change.b.pt + change_delta
|
||||||
else:
|
else:
|
||||||
# otherwise we just shift the window according to the change
|
# otherwise we just shift the window according to the change
|
||||||
txt_b += change_delta
|
txt_b += change_delta
|
||||||
|
|
||||||
reg_a = min(reg_a, change.a.pt) # text region enlarged to the left
|
# the bounding region enlarged to the left
|
||||||
|
reg_a = min(reg_a, change.a.pt)
|
||||||
|
|
||||||
# In this bit, we want to look at the buffer BEFORE the modifications
|
# In this bit, we want to look at the buffer BEFORE the modifications
|
||||||
# but we are working on the buffer modified by all previous changes for each loop
|
# but we are working on the buffer modified by all previous changes for each loop
|
||||||
# we use buffer_shift to keep track of how the buffer shifts around
|
# we use buffer_shift to keep track of how the buffer shifts around
|
||||||
|
@ -198,60 +236,108 @@ def walk_compress_changes(view, changes):
|
||||||
reg_b = change.b.pt + buffer_shift
|
reg_b = change.b.pt + buffer_shift
|
||||||
|
|
||||||
# after using the change delta, we archive it for the next iterations
|
# after using the change delta, we archive it for the next iterations
|
||||||
|
# the minus is just for being able to "add" the buffer shift with a +.
|
||||||
|
# since we encode deleted text as negative in the change_delta, but that requires the shift to the
|
||||||
|
# old position to be positive, and viceversa for text insertion.
|
||||||
buffer_shift -= change_delta
|
buffer_shift -= change_delta
|
||||||
|
|
||||||
# print("\t[buff change]", change.a.pt, change.str, "(", change.len_utf8,")", change.b.pt)
|
# print("\t[buff change]", change.a.pt, change.str, "(", change.len_utf8,")", change.b.pt)
|
||||||
# print("[walking txt]", "[", txt_a, txt_b, "]")
|
|
||||||
# print("[walking reg]", "[", reg_a, reg_b, "]")
|
|
||||||
|
|
||||||
txt = view.substr(sublime.Region(txt_a, txt_b))
|
txt = view.substr(sublime.Region(txt_a, txt_b))
|
||||||
|
print("[walking txt]", "[", txt_a, txt_b, "]", txt)
|
||||||
|
print("[walking reg]", "[", reg_a, reg_b, "]")
|
||||||
return reg_a, txt, reg_b
|
return reg_a, txt, reg_b
|
||||||
|
|
||||||
def apply_buffer_change(text_change):
|
def apply_buffer_change(buffer):
|
||||||
print("test")
|
status_log("registring callback for buffer: {}".format(buffer.primary_view().file_name()))
|
||||||
print(text_change)
|
def buffer_callback(text_change):
|
||||||
print(text_change.start_incl, text_change.end_excl, text_change.content)
|
global _skip_resend
|
||||||
|
# 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.
|
||||||
|
_skip_resend = True
|
||||||
|
view = buffer.primary_view()
|
||||||
|
view.run_command("codemp_replace_text",
|
||||||
|
{"start": text_change.start_incl,
|
||||||
|
"end": text_change.end_excl,
|
||||||
|
"content": text_change.content})
|
||||||
|
|
||||||
|
return buffer_callback
|
||||||
|
|
||||||
|
|
||||||
# Sublime interface
|
# Sublime interface
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
## WIP ##
|
||||||
|
# class CodempSublimeBuffer():
|
||||||
|
|
||||||
|
# def __init__(self, sublime_buffer):
|
||||||
|
# self.buffer = sublime_buffer
|
||||||
|
# self.controller =
|
||||||
|
# self.listener = CodempClientTextChangeListener()
|
||||||
|
# self.skip_resend = False
|
||||||
|
# self.codemp_callback = apply_buffer_change(sublime_buffer)
|
||||||
|
|
||||||
|
# def attach_sublime(self):
|
||||||
|
# self.listener.attach(self.buffer)
|
||||||
|
|
||||||
|
# def detach_sublime(self):
|
||||||
|
# self.listener.detach()
|
||||||
|
## WIP ##
|
||||||
|
|
||||||
class CodempClientViewEventListener(sublime_plugin.ViewEventListener):
|
class CodempClientViewEventListener(sublime_plugin.ViewEventListener):
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_applicable(cls, settings):
|
def is_applicable(cls, settings):
|
||||||
return "codemp_buffer" in settings
|
return ("codemp_buffer" in settings)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def applies_to_primary_view_only(cls):
|
def applies_to_primary_view_only(cls):
|
||||||
return True
|
return False
|
||||||
|
|
||||||
def on_selection_modified_async(self):
|
def on_selection_modified_async(self):
|
||||||
global _cursor_controller
|
global _cursor_controller
|
||||||
if _cursor_controller:
|
if _cursor_controller:
|
||||||
send_cursor(self.view)
|
send_cursor(self.view)
|
||||||
|
|
||||||
def on_close(self):
|
# def on_text_command(self, command, args):
|
||||||
del self.view.settings()["codemp_buffer"]
|
# global _txt_change_listener
|
||||||
|
# print(command, args)
|
||||||
|
# if command == "codemp_replace_text":
|
||||||
|
# print("detaching listener to :", self.view.file_name())
|
||||||
|
# _txt_change_listener.detach()
|
||||||
|
|
||||||
def on_activated_async(self):
|
# return None
|
||||||
#gain input focus
|
|
||||||
pass
|
# def on_post_text_command(self, command, args):
|
||||||
|
# global _txt_change_listener
|
||||||
|
# print(command, args)
|
||||||
|
# if command == "codemp_replace_text":
|
||||||
|
# print("reattaching listener to: ", self.view.file_name())
|
||||||
|
# _text_change_listener.attach(self.view.buffer())
|
||||||
|
|
||||||
|
# return None
|
||||||
|
|
||||||
def on_deactivated_async(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class CodempClientTextChangeListener(sublime_plugin.TextChangeListener):
|
class CodempClientTextChangeListener(sublime_plugin.TextChangeListener):
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_applicable(cls, buffer):
|
def is_applicable(cls, buffer):
|
||||||
if "codemp_buffer" in buffer.primary_view().settings():
|
# don't attach this event listener automatically
|
||||||
return True
|
# we'll do it by hand with .attach(buffer).
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def on_text_changed_async(self, changes):
|
def on_text_changed_async(self, changes):
|
||||||
global _buffer_controller
|
global _buffer_controller
|
||||||
|
global _skip_resend
|
||||||
|
if _skip_resend:
|
||||||
|
_skip_resend = False
|
||||||
|
return
|
||||||
|
|
||||||
if _buffer_controller:
|
if _buffer_controller:
|
||||||
send_buffer_change(self.buffer, changes)
|
send_buffer_change(self.buffer, changes)
|
||||||
|
|
||||||
|
class CodempReplaceTextCommand(sublime_plugin.TextCommand):
|
||||||
|
def run(self, edit, start, end, content):
|
||||||
|
# start included, end excluded
|
||||||
|
self.view.replace(edit, sublime.Region(start, end), content)
|
||||||
|
|
||||||
# See the proxy command class at the bottom
|
# See the proxy command class at the bottom
|
||||||
class CodempConnectCommand(sublime_plugin.WindowCommand):
|
class CodempConnectCommand(sublime_plugin.WindowCommand):
|
||||||
def run(self, server_host):
|
def run(self, server_host):
|
||||||
|
@ -262,11 +348,6 @@ class CodempShareCommand(sublime_plugin.WindowCommand):
|
||||||
def run(self, buffer):
|
def run(self, buffer):
|
||||||
sublime_asyncio.dispatch(share_buffer_command(buffer))
|
sublime_asyncio.dispatch(share_buffer_command(buffer))
|
||||||
|
|
||||||
# see proxy command at the bottom
|
|
||||||
# class CodempJoinCommand(sublime_plugin.WindowCommand):
|
|
||||||
# def run(self, buffer):
|
|
||||||
# sublime_asyncio.dispatch(join_buffer(self.window, buffer))
|
|
||||||
|
|
||||||
class ProxyCodempConnectCommand(sublime_plugin.WindowCommand):
|
class ProxyCodempConnectCommand(sublime_plugin.WindowCommand):
|
||||||
# on_window_command, does not trigger when called from the command palette
|
# on_window_command, does not trigger when called from the command palette
|
||||||
# See: https://github.com/sublimehq/sublime_text/issues/2234
|
# See: https://github.com/sublimehq/sublime_text/issues/2234
|
||||||
|
@ -293,20 +374,6 @@ class ProxyCodempShareCommand(sublime_plugin.WindowCommand):
|
||||||
def input_description(self):
|
def input_description(self):
|
||||||
return 'Share Buffer:'
|
return 'Share Buffer:'
|
||||||
|
|
||||||
|
|
||||||
# class ProxyCodempJoinCommand(sublime_plugin.WindowCommand):
|
|
||||||
# # on_window_command, does not trigger when called from the command palette
|
|
||||||
# # See: https://github.com/sublimehq/sublime_text/issues/2234
|
|
||||||
# def run(self, **kwargs):
|
|
||||||
# self.window.run_command("codemp_join", kwargs)
|
|
||||||
|
|
||||||
# def input(self, args):
|
|
||||||
# if 'buffer' not in args:
|
|
||||||
# return BufferInputHandler()
|
|
||||||
|
|
||||||
# def input_description(self):
|
|
||||||
# return 'Join Buffer:'
|
|
||||||
|
|
||||||
class BufferInputHandler(sublime_plugin.ListInputHandler):
|
class BufferInputHandler(sublime_plugin.ListInputHandler):
|
||||||
def list_items(self):
|
def list_items(self):
|
||||||
ret_list = []
|
ret_list = []
|
||||||
|
|
|
@ -10,8 +10,6 @@ class CodempClient():
|
||||||
await self.handle.connect(server_host)
|
await self.handle.connect(server_host)
|
||||||
|
|
||||||
def disconnect(self): # -> None
|
def disconnect(self): # -> None
|
||||||
# disconnect all buffers
|
|
||||||
# stop all callbacks
|
|
||||||
self.handle = None
|
self.handle = None
|
||||||
|
|
||||||
async def create(self, path, content=None): # -> None
|
async def create(self, path, content=None): # -> None
|
||||||
|
@ -30,7 +28,7 @@ class CodempClient():
|
||||||
async def get_buffer(self, path): # -> BufferController
|
async def get_buffer(self, path): # -> BufferController
|
||||||
return BufferController(await self.handle.get_buffer())
|
return BufferController(await self.handle.get_buffer())
|
||||||
|
|
||||||
async def remove_buffer(self, path): # -> None
|
async def disconnect_buffer(self, path): # -> None
|
||||||
await self.handle.disconnect_buffer(path)
|
await self.handle.disconnect_buffer(path)
|
||||||
|
|
||||||
async def leave_workspace(self): # -> None
|
async def leave_workspace(self): # -> None
|
||||||
|
|
Loading…
Reference in a new issue