dotfiles/.vim/plugin/CoVimClient.vim
2021-07-21 23:14:08 -04:00

364 lines
15 KiB
VimL

"Check for Python Support"
if !has('python3')
com! -nargs=* CoVim echoerr "Error: CoVim requires vim compiled with +python3"
finish
endif
com! -nargs=* CoVim py3 CoVim.command(<f-args>)
"Needs to be set on connect, MacVim overrides otherwise"
function! SetCoVimColors ()
hi CursorUser ctermbg=White ctermfg=Black guibg=White guifg=Black gui=bold term=bold cterm=bold
hi Cursor1 ctermbg=DarkRed ctermfg=White guibg=DarkRed guifg=White gui=bold term=bold cterm=bold
hi Cursor2 ctermbg=DarkBlue ctermfg=White guibg=DarkBlue guifg=White gui=bold term=bold cterm=bold
hi Cursor3 ctermbg=DarkGreen ctermfg=White guibg=DarkGreen guifg=White gui=bold term=bold cterm=bold
hi Cursor4 ctermbg=DarkCyan ctermfg=White guibg=DarkCyan guifg=White gui=bold term=bold cterm=bold
hi Cursor5 ctermbg=DarkMagenta ctermfg=White guibg=DarkMagenta guifg=White gui=bold term=bold cterm=bold
hi Cursor6 ctermbg=Brown ctermfg=White guibg=Brown guifg=White gui=bold term=bold cterm=bold
hi Cursor7 ctermbg=LightRed ctermfg=Black guibg=LightRed guifg=Black gui=bold term=bold cterm=bold
hi Cursor8 ctermbg=LightBlue ctermfg=Black guibg=LightBlue guifg=Black gui=bold term=bold cterm=bold
hi Cursor9 ctermbg=LightGreen ctermfg=Black guibg=LightGreen guifg=Black gui=bold term=bold cterm=bold
hi Cursor10 ctermbg=LightCyan ctermfg=Black guibg=LightCyan guifg=Black gui=bold term=bold cterm=bold
hi Cursor0 ctermbg=LightYellow ctermfg=Black guibg=LightYellow guifg=Black gui=bold term=bold cterm=bold
endfunction
if !exists("CoVim_default_name")
let CoVim_default_name = 0
endif
if !exists("CoVim_default_port")
let CoVim_default_port = 0
endif
python3 << EOF
import vim
import os
import json
import warnings
from twisted.internet.protocol import ClientFactory, Protocol
from twisted.internet import reactor
from threading import Thread
from time import sleep
# Ignore Warnings
warnings.filterwarnings('ignore', '.*', UserWarning)
warnings.filterwarnings('ignore', '.*', DeprecationWarning)
# Find the server path
CoVimServerPath = vim.eval('expand("<sfile>:h")') + '/CoVimServer.py'
## CoVim Protocol
class CoVimProtocol(Protocol):
def __init__(self, fact):
self.fact = fact
def send(self, event):
self.transport.write(event.encode('utf-8'))
def connectionMade(self):
self.send(CoVim.username)
def dataReceived(self, data_string):
def clean_data_string(d_s):
bad_data = d_s.find("}{")
if bad_data > -1:
d_s = d_s[:bad_data+1]
return d_s
if isinstance(data_string, bytes):
data_string = data_string.decode('utf-8')
data_string = clean_data_string(data_string)
packet = json.loads(data_string)
if 'packet_type' in packet.keys():
data = packet['data']
if packet['packet_type'] == 'message':
if data['message_type'] == 'error_newname_taken':
CoVim.disconnect()
print('ERROR: Name already in use. Please try a different name')
if data['message_type'] == 'error_newname_invalid':
CoVim.disconnect()
print('ERROR: Name contains illegal characters. Only numbers, letters, underscores, and dashes allowed. Please try a different name')
if data['message_type'] == 'connect_success':
CoVim.setupWorkspace()
if 'buffer' in data.keys():
CoVim.vim_buffer = data['buffer']
vim.current.buffer[:] = CoVim.vim_buffer
CoVim.addUsers(data['collaborators'])
print('Success! You\'re now connected [Port '+str(CoVim.port)+']')
if data['message_type'] == 'user_connected':
CoVim.addUsers([data['user']])
print(data['user']['name']+' connected to this document')
if data['message_type'] == 'user_disconnected':
CoVim.remUser(data['name'])
print(data['name']+' disconnected from this document')
if packet['packet_type'] == 'update':
if 'buffer' in data.keys() and data['name'] != CoVim.username:
b_data = data['buffer']
CoVim.vim_buffer = vim.current.buffer[:b_data['start']] \
+ b_data['buffer'] \
+ vim.current.buffer[b_data['end']-b_data['change_y']+1:]
vim.current.buffer[:] = CoVim.vim_buffer
if 'updated_cursors' in data.keys():
# We need to update your own cursor as soon as possible, then update other cursors after
for updated_user in data['updated_cursors']:
vim.command(':call matchdelete(' + str(CoVim.collab_manager.collaborators[updated_user['name']][1]) + ')')
vim.command(':call matchadd(\'' + CoVim.collab_manager.collaborators[updated_user['name']][0] + '\', \'\%' + str(updated_user['cursor']['x']) + 'v.\%' + str(updated_user['cursor']['y']) + 'l\', 10, ' + str(CoVim.collab_manager.collaborators[updated_user['name']][1]) + ')')
#for updated_user in data['updated_cursors']:
# if CoVim.username == updated_user['name'] and data['name'] != CoVim.username:
# vim.current.window.cursor = (updated_user['cursor']['y'], updated_user['cursor']['x'])
#data['cursor']['x'] = max(1,data['cursor']['x'])
#print(str(data['cursor']['x'])+', '+str(data['cursor']['y'])
vim.command(':redraw')
#vim.command(':set t_vi=')
#CoVimFactory - Handles Socket Communication
class CoVimFactory(ClientFactory):
def buildProtocol(self, addr):
self.p = CoVimProtocol(self)
return self.p
def startFactory(self):
self.isConnected = True
def stopFactory(self):
self.isConnected = False
def buff_update(self):
d = {
"packet_type": "update",
"data": {
"cursor": {
"x": max(1, vim.current.window.cursor[1]),
"y": vim.current.window.cursor[0]
},
"name": CoVim.username
}
}
d = self.create_update_packet(d)
data = json.dumps(d)
self.p.send(data)
def cursor_update(self):
d = {
"packet_type": "update",
"data": {
"cursor": {
"x": max(1, vim.current.window.cursor[1]+1),
"y": vim.current.window.cursor[0]
},
"name": CoVim.username
}
}
d = self.create_update_packet(d)
data = json.dumps(d)
self.p.send(data)
def create_update_packet(self, d):
current_buffer = vim.current.buffer[:]
if current_buffer != CoVim.vim_buffer:
cursor_y = vim.current.window.cursor[0] - 1
change_y = len(current_buffer) - len(CoVim.vim_buffer)
change_x = 0
if len(CoVim.vim_buffer) > cursor_y-change_y and cursor_y-change_y >= 0 \
and len(current_buffer) > cursor_y and cursor_y >= 0:
change_x = len(current_buffer[cursor_y]) - len(CoVim.vim_buffer[cursor_y-change_y])
limits = {
'from': max(0, cursor_y-abs(change_y)),
'to': min(len(vim.current.buffer)-1, cursor_y+abs(change_y))
}
d_buffer = {
'start': limits['from'],
'end': limits['to'],
'change_y': change_y,
'change_x': change_x,
'buffer': vim.current.buffer[limits['from']:limits['to']+1],
'buffer_size': len(current_buffer)
}
d['data']['buffer'] = d_buffer
CoVim.vim_buffer = current_buffer
return d
def clientConnectionLost(self, connector, reason):
#THIS IS A HACK
if hasattr(CoVim, 'buddylist'):
CoVim.disconnect()
print('Lost connection.')
def clientConnectionFailed(self, connector, reason):
CoVim.disconnect()
print('Connection failed.')
#Manage Collaborators
class CollaboratorManager:
def __init__(self):
self.collab_id_itr = 4
self.reset()
def reset(self):
self.collab_color_itr = 1
self.collaborators = {}
self.buddylist_highlight_ids = []
def addUser(self, user_obj):
#if user_obj['name'] == CoVim.username:
# self.collaborators[user_obj['name']] = ('CursorUser', 4000)
#else:
self.collaborators[user_obj['name']] = ('Cursor' + str(self.collab_color_itr), self.collab_id_itr)
self.collab_id_itr += 1
self.collab_color_itr = (self.collab_id_itr-3) % 11
vim.command(':call matchadd(\''+self.collaborators[user_obj['name']][0]+'\', \'\%' + str(user_obj['cursor']['x']) + 'v.\%'+str(user_obj['cursor']['y'])+'l\', 10, ' + str(self.collaborators[user_obj['name']][1]) + ')')
self.refreshCollabDisplay()
def remUser(self, name):
vim.command('call matchdelete('+str(self.collaborators[name][1]) + ')')
del(self.collaborators[name])
self.refreshCollabDisplay()
def refreshCollabDisplay(self):
buddylist_window_width = int(vim.eval('winwidth(0)'))
CoVim.buddylist[:] = ['']
x_a = 1
line_i = 0
vim.command("1wincmd w")
for match_id in self.buddylist_highlight_ids:
vim.command('call matchdelete('+str(match_id) + ')')
self.buddylist_highlight_ids = []
for name in self.collaborators.keys():
x_b = x_a + len(name)
if x_b > buddylist_window_width:
line_i += 1
x_a = 1
x_b = x_a + len(name)
CoVim.buddylist.append('')
vim.command('resize '+str(line_i+1))
CoVim.buddylist[line_i] += name+' '
self.buddylist_highlight_ids.append(vim.eval('matchadd(\''+self.collaborators[name][0]+'\',\'\%<'+str(x_b)+'v.\%>'+str(x_a)+'v\%'+str(line_i+1)+'l\',10,'+str(self.collaborators[name][1]+2000)+')'))
x_a = x_b + 1
vim.command("wincmd p")
#Manage all of CoVim
class CoVimScope:
def initiate(self, addr, port, name):
#Check if connected. If connected, throw error.
if hasattr(self, 'fact') and self.fact.isConnected:
print('ERROR: Already connected. Please disconnect first')
return
if not port and hasattr(self, 'port') and self.port:
port = self.port
if not addr and hasattr(self, 'addr') and self.addr:
addr = self.addr
if not addr or not port or not name:
print('Syntax Error: Use form :Covim connect <server address> <port> <name>')
return
port = int(port)
addr = str(addr)
vim.command('autocmd VimLeave * py3 CoVim.quit()')
if not hasattr(self, 'connection'):
self.addr = addr
self.port = port
self.username = name
self.vim_buffer = []
self.fact = CoVimFactory()
self.collab_manager = CollaboratorManager()
self.connection = reactor.connectTCP(addr, port, self.fact)
self.reactor_thread = Thread(target=reactor.run, args=(False,))
self.reactor_thread.start()
print('Connecting...')
elif (hasattr(self, 'port') and port != self.port) or (hasattr(self, 'addr') and addr != self.addr):
print('ERROR: Different address/port already used. To try another, you need to restart Vim')
else:
self.collab_manager.reset()
self.connection.connect()
print('Reconnecting...')
def createServer(self, port, name):
vim.command(':silent execute "!'+CoVimServerPath+' '+port+' &>/dev/null &"')
sleep(0.5)
self.initiate('localhost', port, name)
def setupWorkspace(self):
vim.command('call SetCoVimColors()')
vim.command(':autocmd!')
vim.command('autocmd CursorMoved <buffer> py3 reactor.callFromThread(CoVim.fact.cursor_update)')
vim.command('autocmd CursorMovedI <buffer> py3 reactor.callFromThread(CoVim.fact.buff_update)')
vim.command('autocmd VimLeave * py3 CoVim.quit()')
vim.command("1new +setlocal\ stl=%!''")
self.buddylist = vim.current.buffer
self.buddylist_window = vim.current.window
vim.command("wincmd j")
def addUsers(self, userlist):
list(map(self.collab_manager.addUser, userlist))
def remUser(self, name):
self.collab_manager.remUser(name)
def refreshCollabDisplay(self):
self.collab_manager.refreshCollabDisplay()
def command(self, arg1=False, arg2=False, arg3=False, arg4=False):
default_name = vim.eval('CoVim_default_name')
default_name_string = " - default: '"+default_name+"'" if default_name != '0' else ""
default_port = vim.eval('CoVim_default_port')
default_port_string = " - default: "+default_port if default_port != '0' else ""
if arg1 == "connect":
if arg2 and arg3 and arg4:
self.initiate(arg2, arg3, arg4)
elif arg2 and arg3 and default_name != '0':
self.initiate(arg2, arg3, default_name)
elif arg2 and default_port != '0' and default_name != '0':
self.initiate(arg2, default_port, default_name)
else:
print("usage :CoVim connect [host address / 'localhost'] [port"+default_port_string+"] [name"+default_name_string+"]")
elif arg1 == "disconnect":
self.disconnect()
elif arg1 == "quit":
self.exit()
elif arg1 == "start":
if arg2 and arg3:
self.createServer(arg2, arg3)
elif arg2 and default_name != '0':
self.createServer(arg2, default_name)
elif default_port != '0' and default_name != '0':
self.createServer(default_port, default_name)
else:
print("usage :CoVim start [port"+default_port_string+"] [name"+default_name_string+"]")
else:
print("usage: CoVim [start] [connect] [disconnect] [quit]")
def exit(self):
if hasattr(self, 'buddylist_window') and hasattr(self, 'connection'):
self.disconnect()
vim.command('q')
else:
print("ERROR: CoVim must be running to use this command")
def disconnect(self):
if hasattr(self, 'buddylist'):
vim.command("1wincmd w")
vim.command("q!")
self.collab_manager.buddylist_highlight_ids = []
for name in self.collab_manager.collaborators.keys():
if name != CoVim.username:
vim.command(':call matchdelete('+str(self.collab_manager.collaborators[name][1]) + ')')
del(self.buddylist)
if hasattr(self, 'buddylist_window'):
del(self.buddylist_window)
if hasattr(self, 'connection'):
reactor.callFromThread(self.connection.disconnect)
print('Successfully disconnected from document!')
else:
print("ERROR: CoVim must be running to use this command")
def quit(self):
reactor.callFromThread(reactor.stop)
CoVim = CoVimScope()
EOF