codemp-sublime/plugin.py

323 lines
10 KiB
Python
Raw Normal View History

import sublime
import sublime_plugin
# import Codemp.codemp_client as codemp
from Codemp.src.codemp_client import *
import Codemp.ext.sublime_asyncio as sublime_asyncio
import asyncio
import time
# UGLYYYY, find a way to not have global variables laying around.
_tasks = []
_client = None
_cursor_controller = None
_buffer_controller = None
_setting_key = "codemp_buffer"
_regions_colors = [
"region.redish",
"region.orangeish",
"region.yellowish",
"region.greenish",
"region.cyanish",
"region.bluish",
"region.purplish",
"region.pinkish"
]
def status_log(msg):
sublime.status_message("[codemp] {}".format(msg))
print("[codemp] {}".format(msg))
def store_task(name = None):
def store_named_task(task):
global _tasks
task.set_name(name)
_tasks.append(task)
return store_named_task
def get_contents(view):
r = sublime.Region(0, view.size())
return view.substr(r)
def get_matching_view(path):
for window in sublime.windows():
for view in window.views():
if view.file_name() == path:
return view
def rowcol_to_region(view, start, end):
a = view.text_point(start[0], start[1])
b = view.text_point(end[0], end[1])
return sublime.Region(a, b)
def plugin_loaded():
global _client
_client = CodempClient() # create an empty instance of the codemp client.
sublime_asyncio.acquire() # instantiate and start a global asyncio event loop.
def plugin_unloaded():
for window in sublime.windows():
for view in window.views():
if "codemp_buffer" in view.settings():
del view.settings()["codemp_buffer"]
# disconnect all buffers
# stop all callbacks
# disconnect the client.
print("unloading")
async def connect_command(server_host, session="default"):
global _client
status_log("Connecting to {}".format(server_host))
await _client.connect(server_host)
await join_workspace(session)
async def join_workspace(session):
global _client
global _cursor_controller
status_log("Joining workspace: {}".format(session))
_cursor_controller = await _client.join(session)
_cursor_controller.callback(move_cursor)
async def share_buffer_command(buffer):
global _client
global _cursor_controller
global _buffer_controller
status_log("Sharing buffer {}".format(buffer))
view = get_matching_view(buffer)
contents = get_contents(view)
try:
await _client.create(buffer, contents)
_buffer_controller = await _client.attach(buffer)
_buffer_controller.callback(apply_buffer_change)
except Exception as e:
sublime.error_message("Could not share buffer: {}".format(e))
return
status_log("Listening")
view.set_status("z_codemp_buffer", "[Codemp]")
view.settings()["codemp_buffer"] = True
def move_cursor(cursor_event):
global _regions_colors
# 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.
view = get_matching_view(cursor_event.buffer)
if "codemp_buffer" in view.settings():
reg = rowcol_to_region(view, cursor_event.start, cursor_event.end)
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])
def send_cursor(view):
global _cursor_controller
path = view.file_name()
region = view.sel()[0] # TODO: only the last placed cursor/selection.
start = view.rowcol(region.begin()) #only counts UTF8 chars
end = view.rowcol(region.end())
_cursor_controller.send(path, start, end)
def send_buffer_change(buffer, changes):
global _buffer_controller
view = buffer.primary_view()
start, txt, end = compress_changes(view, changes)
contlen = len(_buffer_controller.get_content())
_buffer_controller.delta(start, txt, min(end, contlen))
time.sleep(0.1)
print("server buffer: -------")
print(_buffer_controller.get_content())
def compress_changes(view, changes):
## TODO: doesn't work correctly.
# Sublime text on_text_changed events, gives a list of changes.
# 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
# we receive two split text changes, first we add the new string in front of the selection
# and then we delete the old selection. e.g: [1234] -> hello is split into: [1234] -> hello[1234] -> hello[]
# this fucks over the operations factory algorithm, which panics if reading the operations sequentially,
# since the changes refer to the same point in time and are not updated each time.
# as a workaround, we compress all changes into a big change, which gives the region in which the change occurred
# and the new string, extracted directly from the local buffer already modified.
if len(changes) == 1:
return (changes[0].a.pt, changes[0].str, changes[0].b.pt)
return walk_compress_changes(view, changes)
def walk_compress_changes(view, changes):
# the bounding region of all text changes.
txt_a = float("inf")
txt_b = 0
# the region in the original buffer subjected to the change.
reg_a = float("inf")
reg_b = 0
# we keep track of how much the changes move the indexing of the buffer
buffer_shift = 0 # left - + right
for change in changes:
# the change in characters that the change would bring
# len(str) and .len_utf8 are mutually exclusive
# len(str) is when we insert new text at a position
# .len_utf8 is the length of the deleted/canceled string in the buffer
change_delta = len(change.str) - change.len_utf8
txt_a = min(txt_a, change.a.pt) # the text region is enlarged to the left
# On insertion, change.b.pt == change.a.pt
# If we meet a new insertion further than the current window
# we expand to the right by that change.
# On deletion, change.a.pt == change.b.pt - change.len_utf8
# 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.
if change.b.pt > txt_b:
txt_b = change.b.pt + change_delta
else:
# otherwise we just shift the window according to the change
txt_b += change_delta
reg_a = min(reg_a, change.a.pt) # text region enlarged to the left
# 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
# we use buffer_shift to keep track of how the buffer shifts around
# to map back to the correct index for each change in the unmodified buffer.
if change.b.pt + buffer_shift > reg_b:
# we only enlarge if we have changes that exceede on the right the current window
reg_b = change.b.pt + buffer_shift
# after using the change delta, we archive it for the next iterations
buffer_shift -= change_delta
# 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))
return reg_a, txt, reg_b
def apply_buffer_change(text_change):
print("test")
print(text_change)
print(text_change.start_incl, text_change.end_excl, text_change.content)
# Sublime interface
##############################################################################
class CodempClientViewEventListener(sublime_plugin.ViewEventListener):
@classmethod
def is_applicable(cls, settings):
return "codemp_buffer" in settings
@classmethod
def applies_to_primary_view_only(cls):
return True
def on_selection_modified_async(self):
global _cursor_controller
if _cursor_controller:
send_cursor(self.view)
def on_close(self):
del self.view.settings()["codemp_buffer"]
def on_activated_async(self):
#gain input focus
pass
def on_deactivated_async(self):
pass
class CodempClientTextChangeListener(sublime_plugin.TextChangeListener):
@classmethod
def is_applicable(cls, buffer):
if "codemp_buffer" in buffer.primary_view().settings():
return True
return False
def on_text_changed_async(self, changes):
global _buffer_controller
if _buffer_controller:
send_buffer_change(self.buffer, changes)
# See the proxy command class at the bottom
class CodempConnectCommand(sublime_plugin.WindowCommand):
def run(self, server_host):
sublime_asyncio.dispatch(connect_command(server_host))
# see proxy command at the bottom
class CodempShareCommand(sublime_plugin.WindowCommand):
def run(self, 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):
# 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_connect", kwargs)
def input(self, args):
if 'server_host' not in args:
return ServerHostInputHandler()
def input_description(self):
return 'Server host:'
class ProxyCodempShareCommand(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_share", kwargs)
def input(self, args):
if 'buffer' not in args:
return BufferInputHandler()
def input_description(self):
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):
def list_items(self):
ret_list = []
for window in sublime.windows():
for view in window.views():
if view.file_name():
ret_list.append(view.file_name())
return ret_list
class ServerHostInputHandler(sublime_plugin.TextInputHandler):
def initial_text(self):
return "http://127.0.0.1:50051"