summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorcathook <b01902109@csie.ntu.edu.tw>2014-11-09 02:40:08 +0800
committercathook <b01902109@csie.ntu.edu.tw>2014-11-09 02:40:08 +0800
commitfd1fbcecb1ef75fe8016ab909ae4092ca705b2f1 (patch)
tree252f682763ec8356bc01ebf8c611dcaa0a07515f
parent3f84cb8b7c2aa24e739ae1a02c883e1e4cdb4004 (diff)
downloadvim-shrvim-fd1fbcecb1ef75fe8016ab909ae4092ca705b2f1.tar
vim-shrvim-fd1fbcecb1ef75fe8016ab909ae4092ca705b2f1.tar.gz
vim-shrvim-fd1fbcecb1ef75fe8016ab909ae4092ca705b2f1.tar.bz2
vim-shrvim-fd1fbcecb1ef75fe8016ab909ae4092ca705b2f1.tar.lz
vim-shrvim-fd1fbcecb1ef75fe8016ab909ae4092ca705b2f1.tar.xz
vim-shrvim-fd1fbcecb1ef75fe8016ab909ae4092ca705b2f1.tar.zst
vim-shrvim-fd1fbcecb1ef75fe8016ab909ae4092ca705b2f1.zip
big change
-rw-r--r--server/Makefile12
-rw-r--r--server/setup.py17
-rw-r--r--server/src/.gitignore3
-rw-r--r--server/src/authority_string_transformer.py42
-rw-r--r--server/src/cmd_ui.py227
-rw-r--r--server/src/json_package.py114
-rw-r--r--server/src/log.py33
-rwxr-xr-xserver/src/shared_vim_server.py119
-rw-r--r--server/src/tcp_server.py254
-rw-r--r--server/src/text_chain.py700
-rw-r--r--server/src/users_text_manager.py169
-rw-r--r--test_tool/.gitignore3
-rwxr-xr-xtest_tool/fake_server.py89
l---------test_tool/json_package.py1
l---------test_tool/log.py1
-rwxr-xr-xtest_tool/ping_server.py62
l---------test_tool/tcp_server.py1
l---------test_tool/users_text_manager.py1
-rw-r--r--vim/plugin/shared_vim.vim778
19 files changed, 2626 insertions, 0 deletions
diff --git a/server/Makefile b/server/Makefile
new file mode 100644
index 0000000..a466c70
--- /dev/null
+++ b/server/Makefile
@@ -0,0 +1,12 @@
+
+SRCS = src/*.py
+
+BIN = build/shared_vim_server
+
+.PHONY: build
+
+build: $(BIN)
+
+$(BIN): $(SRCS) setup.py
+ python3 setup.py build
+
diff --git a/server/setup.py b/server/setup.py
new file mode 100644
index 0000000..fe5da73
--- /dev/null
+++ b/server/setup.py
@@ -0,0 +1,17 @@
+import sys
+
+from cx_Freeze import setup, Executable
+
+
+options = {
+ 'build_exe' : {
+ 'build_exe' : 'build/',
+ 'optimize' : 2,
+ },
+}
+
+setup(name='shared_vim_server',
+ version='1.0',
+ description='Shared Vim Server',
+ options=options,
+ executables=[Executable('src/shared_vim_server.py')])
diff --git a/server/src/.gitignore b/server/src/.gitignore
new file mode 100644
index 0000000..22efd1f
--- /dev/null
+++ b/server/src/.gitignore
@@ -0,0 +1,3 @@
+__pycache__
+__pycache__/*
+__pycache__/*.*
diff --git a/server/src/authority_string_transformer.py b/server/src/authority_string_transformer.py
new file mode 100644
index 0000000..8db484b
--- /dev/null
+++ b/server/src/authority_string_transformer.py
@@ -0,0 +1,42 @@
+"""AuthroityStringTransformer."""
+
+from users_text_manager import AUTHORITY
+
+
+class AuthorityStringTransformerError(Exception):
+ """Error raised by AuthorityStringTransformer."""
+ pass
+
+class AuthorityStringTransformer: # pylint:disable=W0232
+ """Transforms authority between number and strin format."""
+ @staticmethod
+ def to_string(authority):
+ """Transform number authority value to string value.
+
+ Args:
+ authority: Authority in number format.
+
+ Returns:
+ authority: Corrosponding authority in string format.
+ """
+ if authority == AUTHORITY.READONLY:
+ return 'RO'
+ elif authority == AUTHORITY.READWRITE:
+ return 'RW'
+ raise AuthorityStringTransformerError('Invalid number.')
+
+ @staticmethod
+ def to_number(string):
+ """Transform string authority value to number value.
+
+ Args:
+ authority: Authority in string format.
+
+ Returns:
+ authority: Corrosponding authority in number format.
+ """
+ if string == 'RO':
+ return AUTHORITY.READONLY
+ elif string == 'RW':
+ return AUTHORITY.READWRITE
+ raise AuthorityStringTransformerError('Invalid string.')
diff --git a/server/src/cmd_ui.py b/server/src/cmd_ui.py
new file mode 100644
index 0000000..ee26115
--- /dev/null
+++ b/server/src/cmd_ui.py
@@ -0,0 +1,227 @@
+"""Command line user interface."""
+
+import cmd
+import re
+import threading
+
+from authority_string_transformer import AuthorityStringTransformer
+from authority_string_transformer import AuthorityStringTransformerError
+from users_text_manager import UsersTextManagerError
+
+INTRO = ''
+PROMPT = '> '
+
+
+class CmdUI(cmd.Cmd): # pylint: disable=R0904
+ """Command line user interface.
+
+ Attributes:
+ _users_text_manager: An instance of UsersTextManager.
+ _tcp_server: An instance of TcpServer.
+ _shared_vim_server: An instance of SharedVimServer.
+ _exit_flag: Whether this UI should stop or not.
+ _thread: Instance of Thread.
+ """
+ def __init__(self, users_text_manager, tcp_server, shared_vim_server):
+ """Constructor.
+
+ Args:
+ users_text_manager: An instance of UsersTextManager.
+ shared_vim_server: An instance of SharedVimServer.
+ """
+ super(CmdUI, self).__init__(INTRO)
+ self.prompt = PROMPT
+ self._users_text_manager = users_text_manager
+ self._tcp_server = tcp_server
+ self._shared_vim_server = shared_vim_server
+ self._stop_flag = False
+ self._thread = None
+
+ def do_add(self, text):
+ """Adds a user, [usage] add <identity> <nickname> <authority>"""
+ try:
+ identity, nickname, authority_str = _split_text(text, 3)
+ authority = AuthorityStringTransformer.to_number(authority_str)
+ self._users_text_manager.add_user(identity, nickname, authority)
+ self.write('Done\n')
+ except _SplitTextError:
+ self.write('Format error!\n' +
+ '[usage] add <identity> <nickname> <authority>\n')
+ except AuthorityStringTransformerError as e:
+ self.write('Fail: %r\n' % e)
+ except UsersTextManagerError as e:
+ self.write('Fail: %r\n' % e)
+
+ def do_delete(self, text):
+ """Deletes a user, [usage] delete <identity>"""
+ try:
+ identity = _split_text(text, 1)[0]
+ self._users_text_manager.delete_user(identity)
+ self.write('Done\n')
+ except _SplitTextError:
+ self.write('Format error!\n' +
+ '[usage] delete <identity>\n')
+ except UsersTextManagerError as e:
+ self.write('Fail: %r\n' % e)
+
+ def do_deleteall(self, text):
+ """Deletes all users, [usage] deleteall"""
+ try:
+ _split_text(text, 0)
+ for identity in self._users_text_manager.get_users_info().keys():
+ self._users_text_manager.delete_user(identity)
+ self.write('Done\n')
+ except _SplitTextError:
+ self.write('Format error!\n' +
+ '[usage] deleteall\n')
+ except UsersTextManagerError as e:
+ self.write('Fail: %r\n' % e)
+
+ def do_list(self, text):
+ """Lists users, [usage] list"""
+ try:
+ _split_text(text, 0)
+ for iden, user in self._users_text_manager.get_users_info().items():
+ self.write('%r => %s' % (iden, user))
+ self.write('Done\n')
+ except _SplitTextError:
+ self.write('Format error!\n' +
+ '[usage] list\n')
+ except UsersTextManagerError as e:
+ self.write('Fail: %r\n' % e)
+
+ def do_load(self, text):
+ """Loads users from a file, [usage] load <filename>"""
+ try:
+ filename = _split_text(text, 1)[0]
+ with open(filename, 'r') as f:
+ while True:
+ line = f.readline()
+ if line.endswith('\n'):
+ line = line[:-1]
+ if not line:
+ break
+ try:
+ iden, nick, auth_str = _split_text(line, 3)
+ auth = AuthorityStringTransformer.to_number(auth_str)
+ self._users_text_manager.add_user(iden, nick, auth)
+ self.write('Done %s %s %s' % (iden, nick, auth))
+ except _SplitTextError:
+ self.write('Error format in the file.')
+ except AuthorityStringTransformerError as e:
+ self.write('Fail: %r\n' % e)
+ except UsersTextManagerError as e:
+ self.write('Fail: %r\n' % e)
+ self.write('Done')
+ except _SplitTextError:
+ self.write('Format error!\n' +
+ '[usage] load <filename>\n')
+ except IOError as e:
+ self.write('Cannot open file? %s' % str(e))
+
+ def do_save(self, text):
+ """Saves users list to file, [usage] save <filename>"""
+ try:
+ filename = _split_text(text, 1)[0]
+ with open(filename, 'w') as f:
+ users_info = self._users_text_manager.get_users_info()
+ for iden, user in users_info.items():
+ auth = AuthorityStringTransformer.to_string(user.authority)
+ f.write('%s %s %s\n' % (iden, user.nick_name, auth))
+ except _SplitTextError:
+ self.write('Format error!\n' +
+ '[usage] save <filename>\n')
+ except IOError as e:
+ self.write('Cannot open file? %s' % str(e))
+
+ def do_port(self, text):
+ """Print the server's port."""
+ _split_text(text, 0)
+ self.write('server port = %r' % self._tcp_server.port)
+
+ def do_exit(self, text):
+ """Exits the program."""
+ self._shared_vim_server.stop()
+
+ def do_echo(self, text): # pylint: disable=R0201
+ """Echo."""
+ print(text)
+
+ def do_help(self, text):
+ """Prints the help document, [usage] help"""
+ commands = ['add', 'delete', 'list', 'load', 'save', 'exit', 'echo',
+ 'help']
+ self.write('commands: \n' + ' '.join(commands))
+
+ def do_EOF(self, text): # pylint: disable=C0103
+ """Same as exit"""
+ self.do_exit(text)
+
+ def emptyline(self):
+ """Do nothing."""
+ pass
+
+ def postcmd(self, unused_stop, unused_text):
+ """Checks whether it should be stop or not."""
+ return self._stop_flag
+
+ def write(self, text):
+ """Writes text by this UI.
+
+ It will call the "do_echo" command.
+
+ Args:
+ text: String to be printed.
+ """
+ self.onecmd('echo ' + text)
+
+ def start(self, init_cmds=None):
+ """Starts this CmdUI.
+
+ Args:
+ init_cmds: Lists of commands to run after startup.
+ """
+ def run_cmdloop(cmd_ui):
+ """Calls the method cmdloop()
+
+ Args:
+ cmd_ui: An instnace of CmdUI.
+ """
+ cmd_ui.cmdloop()
+ self._thread = threading.Thread(target=run_cmdloop, args=(self,))
+ self._thread.start()
+ for c in init_cmds if init_cmds else []:
+ self.onecmd(c)
+
+ def stop(self):
+ """Stops the command line UI."""
+ self._stop_flag = True
+ self.onecmd('echo bye~\n')
+
+ def join(self):
+ """Joins this thread."""
+ self._thread.join()
+
+ def flush(self):
+ """Flush the screen."""
+ pass
+
+
+class _SplitTextError(Exception):
+ """Error raised by the function _split_text()."""
+ pass
+
+def _split_text(text, num):
+ """Split the text into tuple.
+
+ Args:
+ text: The string to be splitted.
+ num: Length of the tuple.
+
+ Return:
+ A num-tuple.
+ """
+ words = [word for word in re.split(r'[ \t]', text) if word]
+ if len(words) != num:
+ raise _SplitTextError()
+ return tuple(words)
diff --git a/server/src/json_package.py b/server/src/json_package.py
new file mode 100644
index 0000000..f01e489
--- /dev/null
+++ b/server/src/json_package.py
@@ -0,0 +1,114 @@
+"""Contains tcp package object."""
+
+import json
+import zlib
+
+
+class JSONPackageError(Exception):
+ """Error raised by JSONPackage."""
+ pass
+
+class JSONPackage(object):
+ """Send/receive json by tcp connection.
+
+ Attribute:
+ content: Content of the package body.
+ """
+ ENCODING = 'utf-8'
+ COMPRESS_LEVEL = 2
+ HEADER_LENGTH = 10
+ def __init__(self):
+ """Constructor."""
+ self.content = None
+
+ def send_to(self, fd):
+ """Sends a string to the tcp-connection.
+
+ Args:
+ fd: Socket fd.
+ """
+ try:
+ string = json.dumps(self.content)
+ body = JSONPackage._create_body_from_string(string)
+ header = JSONPackage._create_header_from_body(body)
+ fd.send(header + body)
+ except TypeError as e:
+ raise JSONPackageError('json: %r' % e)
+
+ def recv_from(self, fd):
+ """Receives a string from the tcp-connection.
+
+ Args:
+ fd: Socket fd.
+ """
+ header = JSONPackage._recv_header_string(fd)
+ body = JSONPackage._recv_body_string(fd, header)
+ try:
+ self.content = json.loads(body)
+ except ValueError as e:
+ raise JSONPackageError('Cannot loads to the json object: %r' % e)
+
+ @staticmethod
+ def _create_body_from_string(string):
+ """Creates package body from data string.
+
+ Args:
+ string: Data string.
+
+ Returns:
+ Package body.
+ """
+ byte_string = string.encode(JSONPackage.ENCODING)
+ return zlib.compress(byte_string, JSONPackage.COMPRESS_LEVEL)
+
+ @staticmethod
+ def _create_header_from_body(body):
+ """Creates package header from package body.
+
+ Args:
+ body: Package body.
+
+ Returns:
+ Package header.
+ """
+ header_string = ('%%0%dd' % JSONPackage.HEADER_LENGTH) % len(body)
+ return header_string.encode(JSONPackage.ENCODING)
+
+ @staticmethod
+ def _recv_header_string(conn):
+ """Receives package header from specified tcp connection.
+
+ Args:
+ conn: The specified tcp connection.
+
+ Returns:
+ Package header.
+ """
+ try:
+ byte = conn.recv(JSONPackage.HEADER_LENGTH)
+ return byte.decode(JSONPackage.ENCODING)
+ except UnicodeError as e:
+ raise JSONPackageError('Cannot decode the header string: %r.' % e)
+
+ @staticmethod
+ def _recv_body_string(conn, header):
+ """Receives package body from specified tcp connection and header.
+
+ Args:
+ conn: The specified tcp connection.
+ header: The package header.
+
+ Returns:
+ Package body.
+ """
+ try:
+ body_length = int(header)
+ body = conn.recv(body_length)
+ body_byte = zlib.decompress(body)
+ return body_byte.decode(JSONPackage.ENCODING)
+ except UnicodeError as e:
+ raise JSONPackageError('Cannot decode the body string: %r.' % e)
+ except ValueError as e:
+ raise JSONPackageError('Cannot get the body_length: %r' % e)
+ except zlib.error as e:
+ raise JSONPackageError('Cannot decompress the body: %r.' % e)
diff --git a/server/src/log.py b/server/src/log.py
new file mode 100644
index 0000000..93ec9bc
--- /dev/null
+++ b/server/src/log.py
@@ -0,0 +1,33 @@
+"""Log information handler."""
+
+import sys
+import threading
+
+
+_lock = threading.Lock()
+
+
+def info(string):
+ """Prints the informations string to the interface.
+
+ Args:
+ string: String to be printed.
+ """
+ with _lock:
+ info.interface.write(string)
+ info.interface.flush()
+
+info.interface = sys.stdout # Interface of the info string to be printed at.
+
+
+def error(string):
+ """Prints the error string to the interface.
+
+ Args:
+ string: String to be printed.
+ """
+ with _lock:
+ error.interface.write(string)
+ error.interface.flush()
+
+error.interface = sys.stderr # Interface of the error string to be printed at.
diff --git a/server/src/shared_vim_server.py b/server/src/shared_vim_server.py
new file mode 100755
index 0000000..a7f5201
--- /dev/null
+++ b/server/src/shared_vim_server.py
@@ -0,0 +1,119 @@
+#! /usr/bin/env python3
+
+"""Main thread."""
+
+import signal
+import sys
+import threading
+import log
+
+from cmd_ui import CmdUI
+from tcp_server import TCPServer
+from users_text_manager import UsersTextManager
+
+
+class _ArgsError(Exception):
+ """Exception raised by _Args."""
+ pass
+
+class _Args(object):
+ """Arguments of this program.
+
+ Attributes:
+ port: Port number.
+ user_list_filename: Default user list.
+ save_filename: Name of the file to save the text.
+ """
+ DOCUMENT = '[usage] <port_number> <user_list_filename> <save_filename>\n'
+ def __init__(self):
+ if len(sys.argv) != 4:
+ raise _ArgsError('Wrong length of arguments.')
+ try:
+ self.port = int(sys.argv[1])
+ except ValueError as e:
+ raise _ArgsError(e)
+ self.user_list_filename = sys.argv[2]
+ self.saved_filename = sys.argv[3]
+
+
+class _SharedVimServerError(Exception):
+ """Error raised by SharedVimServer."""
+ pass
+
+class SharedVimServer(threading.Thread):
+ """Main class.
+
+ Attributes:
+ _users_text_manager: Instance of UsersTextManager.
+ _tcp_server: Instance of TCPServer.
+ _cmd_ui: Instance of CmdUI.
+ """
+ def __init__(self):
+ """Constructor."""
+ super(SharedVimServer, self).__init__()
+ try:
+ self._args = _Args()
+ except _ArgsError as e:
+ raise _SharedVimServerError(str(e) + '\n' + _Args.DOCUMENT)
+ self._users_text_manager = UsersTextManager(self._args.saved_filename)
+ self._tcp_server = TCPServer(self._args.port, self._users_text_manager)
+ self._cmd_ui = CmdUI(self._users_text_manager, self._tcp_server, self)
+ log.info.interface = self._cmd_ui
+ log.error.interface = self._cmd_ui
+
+ def run(self):
+ """Starts the program."""
+ self._tcp_server.start()
+ self._cmd_ui.start(['load %s' % self._args.user_list_filename])
+ self._cmd_ui.join()
+ self._tcp_server.join()
+
+ def stop(self):
+ """Exits the program."""
+ self._cmd_ui.stop()
+ self._tcp_server.stop()
+
+
+class _SignalHandler(object):
+ """Single handler.
+
+ It will handle below the signals:
+ SIGTERM, SIGINT - Exit the program.
+
+ Attributes:
+ _shared_vim_server: Instance of SharedVimServer.
+ """
+ def __init__(self, shared_vim_server):
+ """Constructor.
+
+ Args:
+ shared_vim_server: Instance of SharedVimServer.
+ """
+ self._shared_vim_server = shared_vim_server
+ signal.signal(signal.SIGTERM, self._handler)
+ signal.signal(signal.SIGINT, self._handler)
+
+ def _handler(self, number, unused_frame):
+ """Signal handler function.
+
+ Args:
+ number: The signal number to be handle.
+ """
+ if number in (signal.SIGTERM, signal.SIGINT):
+ self._shared_vim_server.stop()
+
+
+def main():
+ """Program entry point."""
+ try:
+ shared_vim_server = SharedVimServer()
+ _SignalHandler(shared_vim_server)
+ shared_vim_server.start()
+ shared_vim_server.join()
+ except _SharedVimServerError as e:
+ print(e)
+ sys.exit(1)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/server/src/tcp_server.py b/server/src/tcp_server.py
new file mode 100644
index 0000000..9aaf8fe
--- /dev/null
+++ b/server/src/tcp_server.py
@@ -0,0 +1,254 @@
+"""TCP Server."""
+
+import log
+import select
+import socket
+import threading
+import time
+
+from json_package import JSONPackage
+from json_package import JSONPackageError
+from users_text_manager import AUTHORITY
+from users_text_manager import UserInfo
+
+
+FREQUENCY = 8
+
+TIMEOUT = 5
+
+
+class _JSON_TOKEN: # pylint:disable=W0232
+ """Enumeration the Ttken strings for json object."""
+ CURSORS = 'cursors' # other users' cursor position
+ ERROR = 'error' # error string
+ IDENTITY = 'identity' # identity of myself
+ INIT = 'init' # initialize connect flag
+ MODE = 'mode' # vim mode.
+ NICKNAME = 'nickname' # nick name of the user.
+ OTHERS = 'others' # other users info.
+ TEXT = 'text' # text content in the buffer
+
+
+class TCPServer(threading.Thread):
+ """A thread to be the tcp server.
+
+ Attributes:
+ _port: Port number.
+ _sock: Socket fd.
+ _users_text_manager: An instance of UsersTextManager.
+ _stop_flag: Flag for stopping.
+ _connection_handler_threads: List of connction handler threads.
+ """
+ def __init__(self, port, users_text_manager):
+ """Constructor.
+
+ Args:
+ port: Port number.
+ users_text_manager: An instance of UsersTextManager.
+ """
+ super(TCPServer, self).__init__()
+ self._port = port
+ self._sock = None
+ self._users_text_manager = users_text_manager
+ self._stop_flag = False
+ self._connection_handler_threads = []
+
+ @property
+ def port(self):
+ """Gets the port of this server. None for unconnected case."""
+ return self._port if self._sock else None
+
+ def run(self):
+ """Runs the thread."""
+ self._build()
+ self._accept()
+
+ def stop(self):
+ """Stops the thread."""
+ self._stop_flag = True
+ for thr in self._connection_handler_threads:
+ thr.join()
+
+ def _build(self):
+ """Creates the socket."""
+ timeout = 1
+ while not self._stop_flag and not self._sock:
+ try:
+ self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self._sock.bind(('', self._port))
+ self._sock.listen(1024)
+ except socket.error as e:
+ self._sock = None
+ log.error(str(e) + '\n')
+ log.info('Try it %d second(s) later.\n' % timeout)
+ for _ in range(timeout * FREQUENCY):
+ if self._stop_flag:
+ break
+ time.sleep(float(1) / FREQUENCY)
+ timeout *= 2
+ if self._sock:
+ log.info('Successfully built the tcp server.\n')
+
+ def _accept(self):
+ """Accepts the connection and calls the handler."""
+ while not self._stop_flag:
+ readable, _, _ = select.select([self._sock], [], [],
+ float(1) / FREQUENCY)
+ if readable:
+ sock, addr = self._sock.accept()
+ log.info('Client %r connect to server.\n' % str(addr))
+ thr = _TCPConnectionHandler(sock, self._users_text_manager)
+ thr.start()
+ self._connection_handler_threads += [thr]
+
+
+class TCPConnection(object):
+ """My custom tcp connection.
+
+ Args:
+ _conn: The TCP-connection.
+ """
+ def __init__(self, conn):
+ """Constructor.
+
+ Args:
+ conn: TCP-connection.
+ """
+ self._conn = conn
+ self._conn.settimeout(TIMEOUT)
+
+ def send(self, data):
+ """Sends the data until timeout or the socket closed.
+
+ Args:
+ data: Data to be sent.
+ """
+ self._conn.sendall(data)
+
+ def recv(self, nbyte):
+ """Receives the data until timeout or the socket closed.
+
+ Args:
+ nbyte: Bytes of data to receive.
+
+ Return:
+ Bytes of data.
+ """
+ ret = b''
+ while nbyte > 0:
+ recv = self._conn.recv(nbyte)
+ if not recv:
+ raise socket.error('Connection die.')
+ ret += recv
+ nbyte -= len(recv)
+ return ret
+
+ def close(self):
+ """Closes the connection."""
+ self._conn.close()
+
+
+class _TCPConnectionHandler(threading.Thread):
+ """A thread to handle a connection.
+
+ Attributes:
+ _sock: The connection socket.
+ _users_text_manager: An instance of UsersTextManager.
+ """
+ def __init__(self, sock, users_text_manager):
+ """Constructor.
+
+ Args:
+ sock: The connection socket.
+ users_text_manager: An instance of UsersTextManager.
+ """
+ super(_TCPConnectionHandler, self).__init__()
+ self._sock = TCPConnection(sock)
+ self._users_text_manager = users_text_manager
+
+ def run(self):
+ """Runs the thread."""
+ try:
+ json_package = self._receive()
+ json_info = self._sanitize(json_package.content)
+ if json_info:
+ self._handle(json_info)
+ else:
+ self._send({_JSON_TOKEN.ERROR : 'Invalid client.'})
+ except JSONPackageError as e:
+ log.error(str(e))
+ except socket.error as e:
+ log.error(str(e))
+ self._sock.close()
+
+ def _sanitize(self, request):
+ """Sanitizes the request.
+
+ Args:
+ request: The request package.
+
+ Return:
+ Sanitized package.
+ """
+ identity = request[_JSON_TOKEN.IDENTITY]
+ if identity not in self._users_text_manager.get_users_info():
+ return None
+ if request[_JSON_TOKEN.INIT]:
+ self._users_text_manager.reset_user(identity)
+ request[_JSON_TOKEN.TEXT] = ''
+ for mark in request[_JSON_TOKEN.CURSORS]:
+ request[_JSON_TOKEN.CURSORS][mark] = 0
+ else:
+ auth = self._users_text_manager.get_users_info()[identity].authority
+ if auth < AUTHORITY.READWRITE:
+ old_text = self._users_text_manager.get_user_text(identity)
+ request[_JSON_TOKEN.TEXT] = old_text
+ return request
+
+ def _handle(self, request):
+ """Handles the request.
+
+ Args:
+ request: The request package.
+ """
+ identity = request[_JSON_TOKEN.IDENTITY]
+ text = request[_JSON_TOKEN.TEXT]
+ user_info = UserInfo()
+ user_info.mode = request[_JSON_TOKEN.MODE]
+ user_info.cursors = request[_JSON_TOKEN.CURSORS]
+ new_user_info, new_text = self._users_text_manager.update_user_text(
+ identity, user_info, text)
+ response = JSONPackage()
+ response.content = {
+ _JSON_TOKEN.TEXT : new_text,
+ _JSON_TOKEN.CURSORS : new_user_info.cursors,
+ _JSON_TOKEN.MODE : new_user_info.mode,
+ _JSON_TOKEN.OTHERS : [
+ {_JSON_TOKEN.NICKNAME : other.nick_name,
+ _JSON_TOKEN.MODE : other.mode,
+ _JSON_TOKEN.CURSORS: other.cursors}
+ for iden, other in self._users_text_manager.get_users_info(
+ without=[identity], must_online=True).items()]
+ }
+ response.send_to(self._sock)
+
+
+ def _receive(self):
+ """Receive a request.
+
+ Return:
+ The request package.
+ """
+ request = JSONPackage()
+ request.recv_from(self._sock)
+ return request
+
+ def _send(self, pkg):
+ """Sends a response.
+
+ Args:
+ pkg: The package to be sent.
+ """
+ response = JSONPackage()
+ response.content = pkg
+ response.send_to(self._sock)
diff --git a/server/src/text_chain.py b/server/src/text_chain.py
new file mode 100644
index 0000000..59d65c2
--- /dev/null
+++ b/server/src/text_chain.py
@@ -0,0 +1,700 @@
+"""TextChain."""
+
+import difflib
+import log
+
+
+class TextChain(object):
+ """Text chain to handle various between each commit.
+
+ Attributes:
+ _save_filename: Name of the file to stores the content of the buffer.
+ _commits: A list of 2-tuple which likes:
+ first element: The commit id.
+ second element: The instance of _TextCommit.
+ _last_commit: An instance of _TextCommit, cache the last commit for
+ updating the cursor position after commiting.
+ """
+ def __init__(self, save_filename):
+ """Constructor.
+
+ Args:
+ save_filename: Name of the file to save the lastest commit text.
+ """
+ self._save_filename = save_filename
+ content = ''
+ try:
+ with open(save_filename, 'r') as f:
+ content = f.read()
+ except IOError:
+ log.info('Cannot load the default text.')
+ self._commits = [
+ (0, _TextCommit('', '')),
+ (1, _TextCommit('', content))]
+ self._last_commit = None
+
+ def commit(self, orig_id, new_text, cursors):
+ """Commits a update.
+
+ Args:
+ orig_id: Original commit id.
+ new_text: Updated text.
+ cursors: Cursors to rebase at the same time.
+
+ Return:
+ A 3-tuple for new commit id, new text and the rebased cursors.
+ """
+ old_index = self._get_commit_index(orig_id)
+ commit = _TextCommit(self._commits[old_index][1].text, new_text)
+ cursors_info = [commit.get_cursor_info(cur) for cur in cursors]
+ commit.apply_commits(
+ [cmt[1] for cmt in self._commits[old_index + 1 : ]])
+ self._last_commit = commit.copy()
+ new_id = self._commits[-1][0] + 1
+ for info in cursors_info:
+ info.apply_commits([cmt[1]
+ for cmt in self._commits[old_index + 1 : ]])
+ new_cursors = [cursor_info.position for cursor_info in cursors_info]
+ self._commits.append((new_id, commit))
+ self.delete(orig_id)
+ self._save()
+ return new_id, commit.text, new_cursors
+
+ def update_cursors(self, cursors):
+ """Updates the cursors by the last commit.
+
+ Args:
+ cursors: List of cursor position.
+
+ Return:
+ List of updated cursor position.
+ """
+ cursors_info = [_CursorInfo_OnOrigText(cursor) for cursor in cursors]
+ for cursor_info in cursors_info:
+ cursor_info.apply_commits([self._last_commit])
+ return [cursor_info.position for cursor_info in cursors_info]
+
+ def new(self):
+ """Creates an empty commit.
+
+ Return:
+ The commit id of the new commit.
+ """
+ commit_id = self._commits[0][0]
+ self._commits.insert(0, (commit_id - 1, _TextCommit('', '')))
+ return commit_id
+
+ def delete(self, commit_id):
+ """Deletes a commit.
+
+ Args:
+ commit_id: The id of the commit to be delete.
+ """
+ index = self._get_commit_index(commit_id)
+ if index + 1 < len(self._commits):
+ pre_text = self._commits[index - 1][1].text
+ nxt_text = self._commits[index + 1][1].text
+ self._commits[index + 1] = (self._commits[index + 1][0],
+ _TextCommit(pre_text, nxt_text))
+ del self._commits[index]
+
+ def get_text(self, commit_id):
+ """Gets the text of a specified commit.
+
+ Args:
+ commit_id: Id of that commit.
+
+ Return:
+ The text.
+ """
+ return self._commits[self._get_commit_index(commit_id)][1].text
+
+ def _get_commit_index(self, commit_id):
+ """Gets the index of the commits from gived commit id.
+
+ Args:
+ commit_id: Commit id.
+
+ Returns:
+ Index of the corrosponding commit.
+ """
+ for index in range(len(self._commits)):
+ if self._commits[index][0] == commit_id:
+ return index
+
+ def _save(self):
+ """Saves the last text to the file."""
+ try:
+ with open(self._save_filename, 'w') as f:
+ f.write(self._commits[-1][1].text)
+ except IOError:
+ log.info('Cannot save the text to the file.')
+
+
+def _opers_apply_opers(orig_opers, opers_tobe_applied):
+ """Let a list of operations apply another list of operations.
+
+ Args:
+ orig_opers: List of instance of _ChgTextOper.
+ opers_tobe_applied: List of instance of _ChgTextOper.
+
+ Return:
+ A list of instance of _ChgTextOper, which are the ones applied the
+ opers_tobe_applied from the orig_opers.
+ """
+ ret = orig_opers
+ for oper_tobe_applied in opers_tobe_applied:
+ # The operation might split into multiple operations after rebasing,
+ # So here we needs to use another list to stores the new operations.
+ updated_opers = []
+ for orig_oper in ret:
+ updated_opers += orig_oper.apply_oper(oper_tobe_applied)
+ ret = updated_opers
+ return ret
+
+
+class _TextCommit(object):
+ """Stores a text commit.
+
+ It includes a final text after commited and a sequence of _ChgTextOper for
+ changing the original string to the new one.
+
+ Attributes:
+ _text: The final text.
+ _opers: List of operations for changing the original string to the new
+ one.
+ """
+ def __init__(self, old_text, new_text):
+ """Constructor.
+
+ Args:
+ old_text: The original text.
+ new_text: The final text after commited.
+ """
+ self._text = new_text
+ self._opers = []
+ diff = difflib.SequenceMatcher(a=old_text, b=new_text)
+ for tag, begin, end, begin2, end2 in diff.get_opcodes():
+ if tag in ('replace', 'delete', 'insert'):
+ self._opers.append(
+ _ChgTextOper(begin, end, new_text[begin2 : end2]))
+
+ @property
+ def text(self):
+ """Gets the final text after this commit."""
+ return self._text
+
+ @property
+ def opers(self):
+ """Gets the operations of this commit."""
+ return self._opers
+
+ @property
+ def increased_length(self):
+ """Gets the increased length of this commit."""
+ return sum([o.increased_length for o in self._opers])
+
+ def copy(self):
+ """Returns a copy of myself.
+
+ Return:
+ An instance of _TextCommit.
+ """
+ ret = _TextCommit('', '')
+ ret._text = self.text
+ ret._opers = [_ChgTextOper(oper.begin, oper.end, oper.new_text)
+ for oper in self._opers]
+ return ret
+
+ def apply_commits(self, commits):
+ """Applies a list of commits before this occured.
+
+ Args:
+ commits: A list of instance of _TextCommit.
+ """
+ if commits:
+ for commit in commits:
+ self._opers = _opers_apply_opers(self._opers, commit._opers)
+ self._rebase_text(commits[-1].text)
+
+# def squash_before(self, commit):
+# """Squash a commit in to myself which is commited just before me.
+#
+# Args:
+# commit: The commit to be squash.
+# """
+# chg_chars, beg_end_pos = commit._create_chg_info()
+# orig_len = len(commit.text) - commit.increased_length
+# offset = 0
+# # Adds another entry to beg_end_pos to prevent the key out of range
+# beg_end_pos_addend = beg_end_pos + [(orig_len, orig_len + 1)]
+# for oper in self._opers: # Applies my operations on the chg info.
+# beg = beg_end_pos_addend[oper.begin + offset][0]
+# end = beg_end_pos_addend[oper.end + offset][0]
+# length = len(oper.new_text)
+# chg_chars[oper.begin : oper.end] = list(oper.new_text)
+# beg_end_pos[oper.begin : oper.end] = [(beg, end)] * length
+# offset += oper.increased_length
+# self._recreate_opers_from_chg_info(orig_len, chg_chars, beg_end_pos)
+#
+# def _create_chg_info(self):
+# """Creates a list contains change informations.
+#
+# Return:
+# A 2-tuple for two list:
+# first list: List of changed char in the final text,
+# if that position does not change, it will be False.
+# second list: List of 2-tuple with:
+# first element: The begin position of the original text.
+# first element: The end position of the original text.
+# """
+# chg_chars, beg_end_pos = [], []
+# orig_end, orig_len = 0, len(self._text) - self.increased_length
+# for oper in self._opers + [_ChgTextOper(orig_len, orig_len, '')]:
+# chg_chars += [False] * (oper.begin - orig_end)
+# beg_end_pos += [(k, k + 1) for k in range(orig_end, oper.begin)]
+# chg_chars += list(oper.new_text)
+# beg_end_pos += [(oper.begin, oper.end)] * len(oper.new_text)
+# orig_end = oper.end
+# return chg_chars, beg_end_pos
+#
+# def _recreate_opers_from_chg_info(self, orig_len, chg_chars, beg_end_pos):
+# """Creates my operations by gived change information.
+#
+# Args:
+# chg_chars: ...
+# beg_end_pos: ...
+# """
+# self._opers = []
+# chg_chars = [False] + chg_chars + [False]
+# beg_end_pos = [(-1, 0)] + beg_end_pos + [(orig_len, orig_len + 1)]
+# index = 0
+# while index < len(chg_chars) - 1:
+# if chg_chars[index] is not False:
+# index_end = chg_chars.index(False, index) - 1
+# self._opers.append(_ChgTextOper(
+# beg_end_pos[index][0], beg_end_pos[index_end][1],
+# ''.join(chg_chars[index : index_end + 1])))
+# index = index_end
+# if beg_end_pos[index][1] < beg_end_pos[index + 1][0]:
+# self._opers.append(_ChgTextOper(
+# beg_end_pos[index][1], beg_end_pos[index + 1][0], ''))
+# index += 1
+# self._merge_connected_opers()
+#
+# def _merge_connected_opers(self):
+# """Merges the operations who are connected with each other.
+#
+# ex:
+# [ )
+# [ )
+# will become:
+# [ )
+# """
+# ind = 0
+# while ind < len(self._opers) - 1:
+# if self._opers[ind].end == self._opers[ind].begin:
+# self._opers[ind : ind + 1] = [_ChgTextOper(
+# self._opers[ind].begin, self._opers[ind + 1].end,
+# self._opers[ind].new_text + self._opers[ind + 1].new_text)]
+# else:
+# ind += 1
+#
+ def get_cursor_info(self, cursor_pos):
+ """Gets the cursor information by gived cursor position.
+
+ If the cursor position is in a place that will be modified at this
+ commit, it will return _CursorInfo_OnNewCommit; Otherwise it will
+ return _CursorInfo_OnOrigText.
+ Ex:
+ The original text with the only oper be "Chage [4, 9) to another
+ string":
+ 0 1 2 3 4 5 6 7 8 91011121314
+ a b c d[e f g h i]j k l m n o
+ ^ ^ ^ ^ | | | | | | ^ ^ ^ ^ ^ ^
+ Then for the "^", they belone to _CursorInfo_OnOrigText;
+ Otherwise they belone to _CursorInfo_OnNewCommit.
+
+ Args:
+ cursor_pos: Position of the cursor.
+
+ Return:
+ A instance of _CursorInfo_OnNewCommit or _CursorInfo_OnOrigText.
+ """
+ for oper in self._opers:
+ if oper.begin <= cursor_pos <= oper.end:
+ return _CursorInfo_OnNewCommit(oper, cursor_pos - oper.begin)
+ return _CursorInfo_OnOrigText(cursor_pos)
+
+ def _rebase_text(self, new_orig_text):
+ """Rebase the original text to another text.
+
+ Args:
+ new_orig_text: The new text.
+ """
+ end_index = 0
+ self._text = ''
+ for oper in self._opers:
+ self._text += new_orig_text[end_index : oper.begin]
+ self._text += oper.new_text
+ end_index = oper.end
+ self._text += new_orig_text[end_index : ]
+
+
+class _CursorInfo_OnNewCommit(object):
+ """About the cursor position who is at the place changed in the new commit.
+
+ Attributes:
+ _opers: The duplicated operation of the original operation.
+ _delta: The offset between the cursor position and the begin of the
+ operation's range.
+ """
+ def __init__(self, oper, delta):
+ """Constructor.
+
+ Args:
+ oper: The operation which this cursor position is in.
+ delta: The offset the cursor position and the begin of the
+ operation.
+ """
+ # We need to store it in a list because after applying other commits, it
+ # might split into multiple operations.
+ self._opers = [_ChgTextOper(oper.begin, oper.end, oper.new_text)]
+ self._delta = delta
+
+ def apply_commits(self, commits):
+ """Applies commits.
+
+ Does something very similar in commit, because we needs to applies each
+ operations to other commits too.
+
+ Args:
+ commits: List of commits to be applied.
+ """
+ for commit in commits:
+ self._opers = _opers_apply_opers(self._opers, commit.opers)
+
+ @property
+ def position(self):
+ """Calculates and returns the final cursor position."""
+ dt = self._delta
+ for oper in self._opers:
+ if oper.begin + dt <= oper.end:
+ return oper.begin + dt
+ dt -= len(oper.new_text)
+
+
+class _CursorInfo_OnOrigText(object):
+ """About the cursor position who is at the place based on the original text.
+
+ Attributes:
+ _position: The position of the cursor.
+ """
+ def __init__(self, position):
+ """Constructor.
+
+ Args:
+ position: The cursor position.
+ """
+ self._position = position
+
+ def apply_commits(self, commits):
+ """Applies the commit's change on it.
+
+ Args:
+ commits: List of commits to be applied.
+ """
+ for commit in commits:
+ for oper in commit.opers:
+ if self._position <= oper.begin:
+ # Remain changeless when the operation is after the cursor
+ # position.
+ pass
+ elif oper.end - 1 <= self._position:
+ # Just offset to the right place if the operation occures
+ # totally at the left side of the cursor.
+ self._position += oper.increased_length
+ else:
+ # Moves the position to the begin of this operation when the
+ # cursor is inside the operation.
+ self._position = oper.begin
+
+ @property
+ def position(self):
+ """Returns the final cursor position."""
+ return self._position
+
+
+class _ChgTextOper(object):
+ """An operation of changing a text to a new one.
+
+ Here we define changing a text to a new one contains a lot of operations.
+ Each operation will replace a substring in the original text to another
+ text.
+
+ In this class, we will handles that if we want to apply an operation to a
+ text before another operation happened, how to merge and prevent the
+ confliction.
+
+ Attributes:
+ _begin: The begin of the range of the substring in the original string.
+ _end: The end of the range of the substring in the original string.
+ _new_text: The string to replace on.
+
+ Notes:
+ 1. The range is an open range [_begin, _end)
+ """
+
+ def __init__(self, beg, end, new_text):
+ self._begin = beg
+ self._end = end
+ self._new_text = new_text
+
+ def apply_oper(self, oper): # pylint: disable=R0911,R0912
+ """Applies an operation before me.
+
+ Args:
+ oper: An instance of _ChgTextOper.
+
+ Return:
+ A list of instance of _ChgTextOper which contains the equivalent
+ operations after applying the gived operation before it.
+ The reason that it returns a list instead of just an element is that
+ it may be split into multiple operations.
+ """
+ if oper.begin < self._begin:
+ if oper.end <= self._begin:
+ return self._apply_left_seperate(oper)
+ elif oper.end < self._end:
+ return self._apply_left_intersection(oper)
+ elif oper.end == self._end:
+ return self._apply_left_exact_cover(oper)
+ else:
+ return self._apply_total_cover(oper)
+ elif oper.begin == self._begin:
+ if oper.end < self._end:
+ return self._apply_left_exact_inside(oper)
+ elif oper.end == self._end:
+ return self._apply_exact_same(oper)
+ else:
+ return self._apply_right_exact_cover(oper)
+ elif oper.begin < self._end:
+ if oper.end < self._end:
+ return self._apply_exact_inside(oper)
+ elif oper.end == self._end:
+ return self._apply_right_exact_inside(oper)
+ else:
+ return self._apply_right_intersection(oper)
+ else:
+ return self._apply_right_seperate(oper)
+
+ @property
+ def begin(self):
+ """Gets the begin of the range."""
+ return self._begin
+
+ @property
+ def end(self):
+ """Gets the end of the range."""
+ return self._end
+
+ @property
+ def new_text(self):
+ """Gets the string to replace."""
+ return self._new_text
+
+ @property
+ def increased_length(self):
+ """Gets the increased length after done this operation."""
+ return len(self._new_text) - (self._end - self._begin)
+
+ def _apply_left_seperate(self, oper):
+ """Applies the case that,
+
+ The operation: [ )
+ Me: [ )
+ Description: Just offset to the right position, because after that
+ operation done, the length of the new string might be changed.
+
+ Args:
+ oper: Instance of the _ChgTextOper.
+ Return:
+ A list of instance of _ChgTextOper.
+ """
+ offset = oper.increased_length
+ return [_ChgTextOper(self._begin + offset, self._end + offset,
+ self._new_text)]
+
+ def _apply_left_intersection(self, oper):
+ """Applies the case that,
+
+ The operation: [ )
+ Me: [ )
+ Result: [ )
+ Method: Offset the begin of my operaiton to the end of the that
+ operaiont's end.
+
+ Args:
+ oper: Instance of the _ChgTextOper.
+ Return:
+ A list of instance of _ChgTextOper.
+ """
+ offset = oper.increased_length
+ return [_ChgTextOper(oper.end + offset, self._end + offset,
+ self._new_text)]
+
+ def _apply_left_exact_cover(self, oper):
+ """Applies the case that,
+
+ The operation: [ )
+ Me: [ )
+ Result: |
+ (Here "|" means that [ and ) are at the same place)
+ Method: Offset the begin of my operaiton to the end of the that
+ operaiont's end.
+
+ Args:
+ oper: Instance of the _ChgTextOper.
+ Return:
+ A list of instance of _ChgTextOper.
+ """
+ end_pos = oper.end + oper.increased_length
+ return [_ChgTextOper(end_pos, end_pos, self._new_text)]
+
+ def _apply_total_cover(self, oper):
+ """Applies the case that,
+
+ The operation: [ )
+ Me: [ )
+ Result: |
+ (Here "|" means that [ and ) are at the same place)
+ Method: Offset the begin of my operaiton to the end of the that
+ operaiont's end.
+
+ Args:
+ oper: Instance of the _ChgTextOper.
+ Return:
+ A list of instance of _ChgTextOper.
+ """
+ end_pos = oper.end + oper.increased_length
+ return [_ChgTextOper(end_pos, end_pos, self._new_text)]
+
+ def _apply_left_exact_inside(self, oper):
+ """Applies the case that,
+
+ The operation: [ )
+ Me: [ )
+ Result: [ )
+ Method: Offset the begin of my operaiton to the end of the that
+ operaiont's end.
+
+ Args:
+ oper: Instance of the _ChgTextOper.
+ Return:
+ A list of instance of _ChgTextOper.
+ """
+ offset = oper.increased_length
+ return [_ChgTextOper(oper.end + offset, self._end + offset,
+ self._new_text)]
+
+ def _apply_exact_same(self, oper):
+ """Applies the case that,
+
+ The operation: [ )
+ Me: [ )
+ Result: |
+ (Here "|" means that [ and ) are at the same place)
+ Method: Offset the begin of my operaiton to the end of the that
+ operaiont's end.
+
+ Args:
+ oper: Instance of the _ChgTextOper.
+ Return:
+ A list of instance of _ChgTextOper.
+ """
+ offset = oper.increased_length
+ return [_ChgTextOper(oper.end + offset, self._end + offset,
+ self._new_text)]
+
+ def _apply_right_exact_cover(self, oper):
+ """Applies the case that,
+
+ The operation: [ )
+ Me: [ )
+ Result: |
+ (Here "|" means that [ and ) are at the same place)
+ Method: Offset the begin/end of my operation to the end of the that
+ operaiont's end.
+
+ Args:
+ oper: Instance of the _ChgTextOper.
+ Return:
+ A list of instance of _ChgTextOper.
+ """
+ pos = oper.end + oper.increased_length
+ return [_ChgTextOper(pos, pos, self._new_text)]
+
+ def _apply_exact_inside(self, oper):
+ """Applies the case that,
+
+ The operation: [ )
+ Me: [ )
+ Result: [ ) [xx)
+ (Here "[xx)" means that it an operation to replace a range of
+ string with an empty string.)
+ Method: Splits me into two part, the left side remain the original text
+ to replace and the right part will just delete a substring.
+
+ Args:
+ oper: Instance of the _ChgTextOper.
+ Return:
+ A list of instance of _ChgTextOper.
+ """
+ offset = oper.increased_length
+ return [_ChgTextOper(self._begin, oper.begin, self._new_text),
+ _ChgTextOper(oper.end + offset, self._end + offset, '')]
+
+ def _apply_right_exact_inside(self, oper):
+ """Applies the case that,
+
+ The operation: [ )
+ Me: [ )
+ Result: [ )
+ Method: Modifies the end of my operation to the begin of that operation.
+
+ Args:
+ oper: Instance of the _ChgTextOper.
+ Return:
+ A list of instance of _ChgTextOper.
+ """
+ return [_ChgTextOper(self._begin, oper._begin, self._new_text)]
+
+ def _apply_right_intersection(self, oper):
+ """Applies the case that,
+
+ The operation: [ )
+ Me: [ )
+ Result: [ )
+ Method: Modifies the end of my operation to the begin of that operation.
+
+ Args:
+ oper: Instance of the _ChgTextOper.
+ Return:
+ A list of instance of _ChgTextOper.
+ """
+ return [_ChgTextOper(self._begin, oper._begin, self._new_text)]
+
+ def _apply_right_seperate(self, oper):
+ """Applies the case that,
+
+ The operation: [ )
+ Me: [ )
+ Result: [ )
+ Method: Remain unchanged.
+
+ Args:
+ oper: Instance of the _ChgTextOper.
+ Return:
+ A list of instance of _ChgTextOper.
+ """
+ return [_ChgTextOper(self._begin, self._end, self._new_text)]
diff --git a/server/src/users_text_manager.py b/server/src/users_text_manager.py
new file mode 100644
index 0000000..200593e
--- /dev/null
+++ b/server/src/users_text_manager.py
@@ -0,0 +1,169 @@
+"""UsersTextManager."""
+
+import threading
+
+from text_chain import TextChain
+
+_UNKNOWN = -1
+
+class AUTHORITY: # pylint:disable=W0232
+ """Enumeration the types of authority."""
+ READONLY = 1 # can only read.
+ READWRITE = 2 # can read and write.
+
+
+class UserInfo(object):
+ """A pure structor for represent a user's information.
+
+ Attributes:
+ authority: Authority of this user.
+ nick_name: Nick name.
+ mode: Vim mode.
+ cursors: Cursor positions of each mark.
+ last_commit_id: Text commit id.
+ """
+ def __init__(self, authority=_UNKNOWN, nick_name=''):
+ """Constructor.
+
+ Args:
+ authority: Default authority
+ nick_name: Default nick name.
+ """
+ self.authority = authority
+ self.nick_name = nick_name
+ self.mode = _UNKNOWN
+ self.cursors = {}
+ self.last_commit_id = _UNKNOWN
+
+ def __str__(self):
+ return '%s(%r) %r %r' % (
+ self.nick_name, self.authority, self.mode, self.last_commit_id)
+
+
+class UsersTextManagerError(Exception):
+ """Error raised by UsersTextManager."""
+ pass
+
+class UsersTextManager(object):
+ """Handles query/operations about users and texts.
+
+ It main interface between CmdUI/TCPServer and TextChain.
+
+ Attributes:
+ _users: A dict to stores users.
+ key: User identity.
+ value: An instance of UserInfo.
+ _text_chain: An instance of TextChain.
+ _rlock: A threading.RLock to prevent multi-threads access this class at
+ the same time.
+ """
+ def __init__(self, saved_filename):
+ """Constructor.
+
+ Args:
+ saved_filename: Name of the file for TextChain to save the last
+ commit.
+ """
+ self._users = {}
+ self._text_chain = TextChain(saved_filename)
+ self._rlock = threading.RLock()
+
+ def add_user(self, identity, nick_name, authority):
+ """Adds a user.
+
+ Args:
+ identity: Identity of this user.
+ nick_name: Nick name of this user.
+ authority: Authority of this user.
+ """
+ with self._rlock:
+ if identity in self._users:
+ raise UsersTextManagerError('User %r already exists.' %
+ identity)
+ new_user = UserInfo(authority, nick_name)
+ new_user.last_commit_id = self._text_chain.new()
+ self._users[identity] = new_user
+
+ def delete_user(self, identity):
+ """Deletes a user.
+
+ Args:
+ identity: Identity of this user.
+ """
+ with self._rlock:
+ if identity not in self._users:
+ raise UsersTextManagerError('User %r not exists.' % identity)
+ self._text_chain.delete(self._users[identity].last_commit_id)
+ del self._users[identity]
+
+ def reset_user(self, identity):
+ """Resets a user to the initial value.
+
+ Args:
+ identity: Identity of this user.
+ """
+ with self._rlock:
+ authority = self._users[identity].authority
+ nick_name = self._users[identity].nick_name
+ self.delete_user(identity)
+ self.add_user(identity, nick_name, authority)
+
+ def get_users_info(self, without=None, must_online=False):
+ """Gets the users informations.
+
+ Args:
+ without: Blacklist.
+ must_online: A flag for whether just returns the one online or not.
+
+ Return:
+ A dict with key=authority, value=instance of UserInfo.
+ """
+ with self._rlock:
+ without = without if without is not None else []
+ online_check = lambda x: (x != _UNKNOWN if must_online else True)
+ return dict([pair for pair in self._users.items()
+ if pair[0] not in without and \
+ online_check(pair[1].mode)])
+
+ def update_user_text(self, identity, new_user_info, new_text):
+ """Updates a user's information with new information and text.
+
+ Args:
+ new_user_info: An instance of UserInfo.
+ new_text: New text.
+
+ Return:
+ A 2-tuple for a instance of UserInfo and a string.
+ """
+ with self._rlock:
+ curlist = list(new_user_info.cursors.items())
+ new_commit_id, new_text, new_cursors = self._text_chain.commit(
+ self._users[identity].last_commit_id, new_text,
+ [pos for mark, pos in curlist])
+ curlist = [(curlist[i][0], new_cursors[i])
+ for i in range(len(curlist))]
+ self._users[identity].last_commit_id = new_commit_id
+ self._users[identity].mode = new_user_info.mode
+ self._users[identity].cursors = dict(curlist)
+ for iden, user in self._users.items():
+ if iden == identity:
+ continue
+ curs_info = list(user.cursors.items())
+ new_curs = self._text_chain.update_cursors(
+ [pos for mark, pos in curs_info])
+ user.cursors = dict([(curs_info[i][0], new_curs[i])
+ for i in range(len(new_curs))])
+ return (self._users[identity], new_text)
+
+ def get_user_text(self, identity):
+ """Gets the last commit text of a specified user.
+
+ Args:
+ identity: The identity of that user.
+
+ Return:
+ The text.
+ """
+ with self._rlock:
+ return self._text_chain.get_text(self._users[identity].
+ last_commit_id)
diff --git a/test_tool/.gitignore b/test_tool/.gitignore
new file mode 100644
index 0000000..22efd1f
--- /dev/null
+++ b/test_tool/.gitignore
@@ -0,0 +1,3 @@
+__pycache__
+__pycache__/*
+__pycache__/*.*
diff --git a/test_tool/fake_server.py b/test_tool/fake_server.py
new file mode 100755
index 0000000..b27d14d
--- /dev/null
+++ b/test_tool/fake_server.py
@@ -0,0 +1,89 @@
+#! /usr/bin/env python3
+
+import copy
+import log
+import sys
+import tcp_server
+import threading
+
+from tcp_server import _JSON_TOKEN
+from json_package import JSONPackage
+
+class MODE: # pylint:disable=W0232
+ """Enumeration type of mode."""
+ NORMAL = 1 # normal mode.
+ INSERT = 2 # insert mode.
+ REPLACE = 3 # replace mode.
+ VISUAL = 4 # visual mode.
+ LINE_VISUAL = 5 # line visual mode.
+ BLOCK_VISUAL = 6 # block visual mode.
+
+counter = 0
+
+class _TCPConnectionHandler(threading.Thread):
+ """A thread to handle a connection.
+
+ Attributes:
+ _sock: The connection socket.
+ _users_text_manager: An instance of UsersTextManager.
+ """
+ def __init__(self, sock, users_text_manager):
+ """Constructor.
+
+ Args:
+ sock: The connection socket.
+ users_text_manager: An instance of UsersTextManager.
+ """
+ super(_TCPConnectionHandler, self).__init__()
+ self._sock = sock
+ self._users_text_manager = users_text_manager
+
+ def run(self):
+ """Runs the thread."""
+ request = self._receive()
+ self._handle(request)
+ self._sock.close()
+
+
+ def _handle(self, request):
+ """Handles the request.
+
+ Args:
+ request: The request package.
+ """
+ global counter
+ fake_user1 = copy.deepcopy(request.content)
+ fake_user1[_JSON_TOKEN.NICKNAME] = 'user1'
+ del fake_user1[_JSON_TOKEN.TEXT]
+ fake_user1[_JSON_TOKEN.MODE] = MODE.INSERT
+ fake_user1[_JSON_TOKEN.CURSORS]['.'] = 0
+ counter += 1
+ fake_user2 = copy.deepcopy(request.content)
+ fake_user2[_JSON_TOKEN.NICKNAME] = 'user2'
+ del fake_user2[_JSON_TOKEN.TEXT]
+ fake_user2[_JSON_TOKEN.MODE] = MODE.LINE_VISUAL
+ fake_user2[_JSON_TOKEN.CURSORS]['.'] = 62
+ fake_user2[_JSON_TOKEN.CURSORS]['v'] = 3
+ request.content[_JSON_TOKEN.OTHERS] = [fake_user1, fake_user2]
+ log.info(str(request.content) + '\n')
+ request.send_to(self._sock)
+
+ def _receive(self):
+ """Receive a request.
+
+ Return:
+ The request package.
+ """
+ request = JSONPackage()
+ request.recv_from(self._sock)
+ return request
+
+
+tcp_server._TCPConnectionHandler = _TCPConnectionHandler
+
+def main():
+ server = tcp_server.TCPServer(int(sys.argv[1]), None)
+ server.start()
+
+if __name__ == '__main__':
+ main()
diff --git a/test_tool/json_package.py b/test_tool/json_package.py
new file mode 120000
index 0000000..a1b4936
--- /dev/null
+++ b/test_tool/json_package.py
@@ -0,0 +1 @@
+../server/src/json_package.py \ No newline at end of file
diff --git a/test_tool/log.py b/test_tool/log.py
new file mode 120000
index 0000000..52e2b3d
--- /dev/null
+++ b/test_tool/log.py
@@ -0,0 +1 @@
+../server/src/log.py \ No newline at end of file
diff --git a/test_tool/ping_server.py b/test_tool/ping_server.py
new file mode 100755
index 0000000..5cdf41b
--- /dev/null
+++ b/test_tool/ping_server.py
@@ -0,0 +1,62 @@
+#! /usr/bin/env python3
+
+import log
+import sys
+import tcp_server
+import threading
+
+from json_package import JSONPackage
+
+class _TCPConnectionHandler(threading.Thread):
+ """A thread to handle a connection.
+
+ Attributes:
+ _sock: The connection socket.
+ _users_text_manager: An instance of UsersTextManager.
+ """
+ def __init__(self, sock, users_text_manager):
+ """Constructor.
+
+ Args:
+ sock: The connection socket.
+ users_text_manager: An instance of UsersTextManager.
+ """
+ super(_TCPConnectionHandler, self).__init__()
+ self._sock = sock
+ self._users_text_manager = users_text_manager
+
+ def run(self):
+ """Runs the thread."""
+ request = self._receive()
+ self._handle(request)
+ self._sock.close()
+
+
+ def _handle(self, request):
+ """Handles the request.
+
+ Args:
+ request: The request package.
+ """
+ log.info(str(request.content) + '\n')
+ request.send_to(self._sock)
+
+ def _receive(self):
+ """Receive a request.
+
+ Return:
+ The request package.
+ """
+ request = JSONPackage()
+ request.recv_from(self._sock)
+ return request
+
+
+tcp_server._TCPConnectionHandler = _TCPConnectionHandler
+
+def main():
+ server = tcp_server.TCPServer(int(sys.argv[1]), None)
+ server.start()
+
+if __name__ == '__main__':
+ main()
diff --git a/test_tool/tcp_server.py b/test_tool/tcp_server.py
new file mode 120000
index 0000000..15e5f1d
--- /dev/null
+++ b/test_tool/tcp_server.py
@@ -0,0 +1 @@
+../server/src/tcp_server.py \ No newline at end of file
diff --git a/test_tool/users_text_manager.py b/test_tool/users_text_manager.py
new file mode 120000
index 0000000..9887bee
--- /dev/null
+++ b/test_tool/users_text_manager.py
@@ -0,0 +1 @@
+../server/src/users_text_manager.py \ No newline at end of file
diff --git a/vim/plugin/shared_vim.vim b/vim/plugin/shared_vim.vim
new file mode 100644
index 0000000..3b949d7
--- /dev/null
+++ b/vim/plugin/shared_vim.vim
@@ -0,0 +1,778 @@
+function! SharedVimConnect(server_name, port, identity)
+ let b:shared_vim_server_name = a:server_name
+ let b:shared_vim_port = a:port
+ let b:shared_vim_identity = a:identity
+ let b:shared_vim_init = 1
+ call SharedVimSync()
+endfunction
+
+
+function! SharedVimDisconnect()
+ unlet! b:shared_vim_server_name
+ unlet! b:shared_vim_port
+ unlet! b:shared_vim_identity
+ unlet! b:shared_vim_init
+endfunction
+
+
+function! SharedVimSync()
+python << EOF
+import bisect
+import json
+import re
+import socket
+import vim
+import zlib
+
+TIMEOUT = 5
+
+NUM_GROUPS = 5
+NORMAL_CURSOR_GROUPS = ['SharedVimNor%d' % i for i in range(1, NUM_GROUPS + 1)]
+INSERT_CURSOR_GROUPS = ['SharedVimIns%d' % i for i in range(1, NUM_GROUPS + 1)]
+VISUAL_GROUPS = ['SharedVimVbk%d' % i for i in range(1, NUM_GROUPS + 1)]
+
+class CURSOR_MARK: # pylint:disable=W0232
+ """Enumeration type of cursor marks."""
+ CURRENT = '.'
+ V = 'v'
+
+class MATCH_PRI: # pylint:disable=W0232
+ """Enumerations types of match priority."""
+ NORMAL = 2
+ INSERT = 3
+ VISUAL = 1
+
+class MODE: # pylint:disable=W0232
+ """Enumeration type of mode."""
+ NORMAL = 1 # normal mode.
+ INSERT = 2 # insert mode.
+ REPLACE = 3 # replace mode.
+ VISUAL = 4 # visual mode.
+ LINE_VISUAL = 5 # line visual mode.
+ BLOCK_VISUAL = 6 # block visual mode.
+
+class _JSON_TOKEN: # pylint:disable=W0232
+ """Enumeration the Ttken strings for json object."""
+ CURSORS = 'cursors' # other users' cursor position
+ ERROR = 'error' # error string
+ IDENTITY = 'identity' # identity of myself
+ INIT = 'init' # initialize connect flag
+ MODE = 'mode' # vim mode.
+ NICKNAME = 'nickname' # nick name of the user.
+ OTHERS = 'others' # other users info.
+ TEXT = 'text' # text content in the buffer
+
+
+class JSONPackage(object):
+ """Send/receive json by tcp connection.
+
+ Attribute:
+ content: Content of the package body.
+ """
+ ENCODING = 'utf-8'
+ COMPRESS_LEVEL = 2
+ HEADER_LENGTH = 10
+ def __init__(self):
+ """Constructor."""
+ self.content = None
+
+ def send_to(self, fd):
+ """Sends a string to the tcp-connection.
+
+ Args:
+ fd: Socket fd.
+ """
+ string = json.dumps(self.content)
+ body = JSONPackage._create_body_from_string(string)
+ header = JSONPackage._create_header_from_body(body)
+ fd.send(header + body)
+
+ def recv_from(self, fd):
+ """Receives a string from the tcp-connection.
+
+ Args:
+ fd: Socket fd.
+ """
+ header = JSONPackage._recv_header_string(fd)
+ body = JSONPackage._recv_body_string(fd, header)
+ self.content = json.loads(body)
+
+ @staticmethod
+ def _create_body_from_string(string):
+ """Creates package body from data string.
+
+ Args:
+ string: Data string.
+
+ Returns:
+ Package body.
+ """
+ byte_string = string.encode(JSONPackage.ENCODING)
+ return zlib.compress(byte_string, JSONPackage.COMPRESS_LEVEL)
+
+ @staticmethod
+ def _create_header_from_body(body):
+ """Creates package header from package body.
+
+ Args:
+ body: Package body.
+
+ Returns:
+ Package header.
+ """
+ header_string = ('%%0%dd' % JSONPackage.HEADER_LENGTH) % len(body)
+ return header_string.encode(JSONPackage.ENCODING)
+
+ @staticmethod
+ def _recv_header_string(conn):
+ """Receives package header from specified tcp connection.
+
+ Args:
+ conn: The specified tcp connection.
+
+ Returns:
+ Package header.
+ """
+ byte = conn.recv(JSONPackage.HEADER_LENGTH)
+ return byte.decode(JSONPackage.ENCODING)
+
+ @staticmethod
+ def _recv_body_string(conn, header):
+ """Receives package body from specified tcp connection and header.
+
+ Args:
+ conn: The specified tcp connection.
+ header: The package header.
+
+ Returns:
+ Package body.
+ """
+ body_length = int(header)
+ body = conn.recv(body_length)
+ body_byte = zlib.decompress(body)
+ return body_byte.decode(JSONPackage.ENCODING)
+
+
+class _VimVarInfo(object):
+ """Gets/sets the variable in vim."""
+ def __init__(self):
+ """Constructor."""
+ pass
+
+ def __getattr__(self, variable_name):
+ """Gets the specified vim variable.
+
+ Args:
+ variable_name: Variable name.
+
+ Returns:
+ None if the value is not exists, otherwise the value.
+ """
+ if variable_name not in vim.current.buffer.vars:
+ return None
+ ret = vim.current.buffer.vars[variable_name]
+ if isinstance(ret, bytes):
+ return ret.decode(JSONPackage.ENCODING)
+ return ret
+
+ def __setattr__(self, variable_name, value):
+ """Sets the specifiec vim variable.
+
+ Args:
+ variable_name: Variable name.
+ value: Value.
+ """
+ vim.current.buffer.vars[variable_name] = value
+
+ def __getitem__(self, variable_name):
+ """Gets the specified vim variable.
+
+ Args:
+ variable_name: Variable name.
+
+ Returns:
+ None if the value is not exists, otherwise the value.
+ """
+ return self.__getattr__(variable_name)
+
+ def __setitem__(self, variable_name, value):
+ """Sets the specifiec vim variable.
+
+ Args:
+ variable_name: Variable name.
+ value: Value.
+ """
+ self.__setattr__(variable_name, value)
+
+ def __delitem__(self, variable_name):
+ """Deletes the specified vim variable.
+
+ Args:
+ variable_name: Variable name.
+ """
+ del vim.current.buffer.vars[variable_name]
+
+
+class _VimCursorsInfo(object):
+ """Gets/sets the cursor in vim.
+
+ Attributes:
+ _info: A instance of VimInfo.
+ """
+ def __init__(self, info):
+ """Constructor.
+
+ Args:
+ info: An instance of VimInfo.
+ """
+ self._info = info
+
+ def __getitem__(self, mark):
+ """Gets the cursor position.
+
+ Args:
+ mark: Which cursor.
+
+ Return:
+ Cursor position.
+ """
+ pos = [int(x) for x in vim.eval('getpos("%s")' % mark)]
+ return self._info.rc_to_num((pos[1] - 1, pos[2] - 1))
+
+ def __setitem__(self, mark, value):
+ """Sets the cursor position.
+
+ Args:
+ mark: Which cursor.
+ """
+ pos = self._info.num_to_rc(value)
+ if mark == CURSOR_MARK.V:
+ mark = CURSOR_MARK.CURRENT
+ vim.eval('setpos("%s", [0, %d, %d, 0])' %
+ (mark, pos[0] + 1, pos[1] + 1))
+
+
+class _GroupInfo(object):
+ """Higilights informations about a user group.
+
+ Attributes:
+ _normal_cursor_positions: List of cursor positions in normal mode.
+ _insert_cursor_positions: List of cursor positions in insert mode.
+ _visual_positions: List of positions in visual blocks.
+ """
+ def __init__(self):
+ """Constructor."""
+ self._normal_cursor_positions = []
+ self._insert_cursor_positions = []
+ self._visual_positions = []
+
+ def set_mode_cursor(self, mode, rc):
+ """Sets the mode and the cursor.
+
+ Args:
+ mode: The mode.
+ rc: The cursor position.
+ """
+ if mode in (MODE.INSERT, MODE.REPLACE):
+ self._insert_cursor_positions += [rc]
+ else:
+ self._normal_cursor_positions += [rc]
+
+ def add_visual(self, rc):
+ """Add a visual position.
+
+ Args:
+ rc: The position.
+ """
+ self._visual_positions += [rc]
+
+ @property
+ def normal_cursor_positions(self):
+ return self._normal_cursor_positions
+
+ @property
+ def insert_cursor_positions(self):
+ return self._insert_cursor_positions
+
+ @property
+ def visual_positions(self):
+ return self._visual_positions
+
+
+class _VimHighlightInfo(object):
+ """Highlight informations about users.
+
+ Attributes:
+ _info: A instance of VimInfo.
+ _groups: A list of instance of GroupInfo.
+ _username_to_group: A dict for mapping the username to the instance of
+ GroupInfo.
+ """
+ def __init__(self, info):
+ """Constructor.
+
+ Args:
+ info: An instance of VimInfo.
+ """
+ self._info = info
+ self._groups = [_GroupInfo() for unused_i in range(NUM_GROUPS)]
+ self._username_to_group = {}
+
+ def __getitem__(self, name):
+ """Gets the cursor position.
+
+ Args:
+ name: User name.
+
+ Return:
+ Cursor position.
+ """
+ return self._username_to_group[name]
+
+ def _get_group_id(self, string):
+ """Transform the gived string to a valid group index.
+
+ Args:
+ string: The gived string.
+
+ Return:
+ The index in range(0, NUM_GROUPS)
+ """
+ x = 0
+ for c in string:
+ x = (x * 23 + ord(c)) % NUM_GROUPS
+ return x
+
+ def reset(self, usernames):
+ """Reset the users.
+
+ Args:
+ usernames: A list of username.
+ """
+ self._username_to_group = {}
+ for name in usernames:
+ gid = self._get_group_id(name)
+ self._username_to_group[name] = self._groups[gid]
+
+ def render(self):
+ """Render the highlight to vim."""
+ for index in range(NUM_GROUPS):
+ self._info.match(NORMAL_CURSOR_GROUPS[index], MATCH_PRI.NORMAL,
+ self._groups[index].normal_cursor_positions)
+ self._info.match(INSERT_CURSOR_GROUPS[index], MATCH_PRI.INSERT,
+ self._groups[index].insert_cursor_positions)
+ self._info.match(VISUAL_GROUPS[index], MATCH_PRI.VISUAL,
+ self._groups[index].visual_positions)
+
+
+class VimInfo(object):
+ """Gets/sets the information about vim.
+
+ Attributes:
+ _cursor_info: An instance of _VimCursorInfo.
+ _highlight_info: An instance of _VimHighlightInfo.
+ _mode: Last mode.
+ _text_num_sum: ...
+ _var_info: An instance of VimVarInfo.
+ """
+ def __init__(self):
+ """Constructor."""
+ self._var_info = _VimVarInfo()
+ self._cursor_info = _VimCursorsInfo(self)
+ self._highlight_info = _VimHighlightInfo(self)
+ self._text_num_sum = []
+ self._mode = None
+ self._calc_text_num_sum(vim.current.buffer[:])
+
+ @property
+ def text(self):
+ """Gets the buffer text."""
+ return '\n'.join(vim.current.buffer[:])
+
+ @text.setter
+ def text(self, value):
+ """Sets the buffer text."""
+ lines = re.split('\n', value)
+ if not lines:
+ lines = ['']
+ buflen = len(vim.current.buffer)
+ vim.current.buffer[0 : buflen] = lines
+ self._calc_text_num_sum(lines)
+
+ @property
+ def mode(self):
+ """Gets the current mode."""
+ mode_str = vim.eval('mode()')
+ if mode_str == 'i':
+ self._mode = MODE.INSERT
+ elif mode_str == 'R':
+ self._mode = MODE.REPLACE
+ elif mode_str == 'v':
+ self._mode = MODE.VISUAL
+ elif mode_str == 'V':
+ self._mode = MODE.LINE_VISUAL
+ elif len(mode_str) == 1 and ord(mode_str) == 22:
+ self._mode = MODE.BLOCK_VISUAL
+ else:
+ self._mode = MODE.NORMAL
+ return self._mode
+
+ @mode.setter
+ def mode(self, value):
+ """Sets the current mode."""
+ if self._mode != value:
+ if value == MODE.INSERT:
+ vim.command('startinsert')
+ elif value == MODE.REPLACE:
+ vim.command('startreplace')
+ elif value == MODE.VISUAL:
+ vim.command('exe "norm! v"')
+ elif value == MODE.LINE_VISUAL:
+ vim.command('exe "norm! V"')
+ elif value == MODE.BLOCK_VISUAL:
+ vim.command('exe "norm! %c"' % 22)
+ else:
+ vim.command('exe "norm! \\<esc>"')
+ self._mode = value
+
+ @property
+ def cursors(self):
+ """Gets/sets the cursor information. Delegates to VimCursorInfo."""
+ return self._cursor_info
+
+ @property
+ def highlight(self):
+ """Gets/sets the highlight info. Delegates to VimHighlightInfo."""
+ return self._highlight_info
+
+ def match(self, group_name, priority, positions):
+ """Set the match informations.
+
+ Args:
+ group_name: Group name.
+ priority: Priority for the vim function matchadd().
+ positions: List of row-column position.
+ """
+ last_id = self.var['shared_vim_' + group_name]
+ if last_id is not None and last_id > 0:
+ ret = vim.eval('matchdelete(%d)' % int(last_id))
+ del self.var['shared_vim_' + group_name]
+ if positions:
+ rcs = [(rc[0] + 1, rc[1] + 1) for rc in positions]
+ patterns = '\\|'.join(['\\%%%dl\\%%%dc' % rc for rc in rcs])
+ mid = int(vim.eval("matchadd('%s', '%s', %d)" %
+ (group_name, patterns, priority)))
+ if mid != -1:
+ self.var['shared_vim_' + group_name] = mid
+
+ @property
+ def var(self):
+ """Gets/sets the var information. Delegates to VimVarInfo."""
+ return self._var_info
+
+ def rowlen(self, row):
+ """Gets the length of a specified row.
+
+ Args:
+ row: The specified line row.
+
+ Return:
+ Length of that row.
+ """
+ prev = 0 if row == 0 else self._text_num_sum[row - 1]
+ return self._text_num_sum[row] - prev - 1
+
+ def num_to_rc(self, num, rmin=0):
+ """Transforms byte position to row-column cursor position.
+
+ Args:
+ num: byte cursor position.
+
+ Return:
+ List of row-column position.
+ """
+ row = bisect.bisect_right(self._text_num_sum, num, lo=rmin)
+ col = num - (0 if row == 0 else self._text_num_sum[row - 1])
+ return (row, col)
+
+ def nums_to_rcs(self, nums):
+ """Transforms list of sorted byte positions.
+
+ Args:
+ nums: list of byte cursor positions.
+
+ Return:
+ List of row-column positions.
+ """
+ ret = []
+ last_r = 0
+ for num in nums:
+ my_rc = self.num_to_rc(num, rmin=last_r)
+ ret += [my_rc]
+ last_r = my_rc[0]
+ return ret
+
+ def rc_to_num(self, rc):
+ """Transforms row-column cursor position to bytes position.
+
+ Args:
+ rc: Row-column cursor position.
+
+ Return:
+ byte position.
+ """
+ return rc[1] + (self._text_num_sum[rc[0] - 1] if rc[0] > 0 else 0)
+
+ def _calc_text_num_sum(self, lines):
+ """Calculates the sum bytes of each line.
+
+ Args:
+ lines: Lines of text.
+ """
+ self._text_num_sum, pre = [], 0
+ for index in range(len(lines)):
+ self._text_num_sum += [pre + len(lines[index]) + 1]
+ pre += len(lines[index]) + 1
+
+
+class TCPConnection(object):
+ """My custom tcp connection.
+
+ Args:
+ _conn: The TCP-connection.
+ """
+ def __init__(self, conn):
+ """Constructor.
+
+ Args:
+ conn: TCP-connection.
+ """
+ self._conn = conn
+ self._conn.settimeout(TIMEOUT)
+
+ def send(self, data):
+ """Sends the data until timeout or the socket closed.
+
+ Args:
+ data: Data to be sent.
+ """
+ self._conn.sendall(data)
+
+ def recv(self, nbyte):
+ """Receives the data until timeout or the socket closed.
+
+ Args:
+ nbyte: Bytes of data to receive.
+
+ Return:
+ Bytes of data.
+ """
+ ret = b''
+ while nbyte > 0:
+ recv = self._conn.recv(nbyte)
+ if not recv:
+ raise socket.error('Connection die.')
+ ret += recv
+ nbyte -= len(recv)
+ return ret
+
+ def close(self):
+ """Closes the connection."""
+ self._conn.close()
+
+
+class TCPClientError(Exception):
+ """Exception raised by TCPClient."""
+ pass
+
+class TCPClient(object):
+ """TCP client.
+
+ Attributes:
+ _sock: Connection.
+ """
+ def __init__(self, vim_info):
+ """Constructor, automatically connects to the server.
+
+ Args:
+ vim_info: An instane of VimInfo.
+ """
+ try:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.connect((vim_info.var.shared_vim_server_name,
+ vim_info.var.shared_vim_port))
+ self._sock = TCPConnection(sock)
+ except TypeError as e:
+ raise TCPClientError('Cannot connect to server: %r' % e)
+ except socket.error as e:
+ raise TCPClientError('Cannot connect to server: %r' % e)
+
+ def request(self, req):
+ """Sends a request to server and get the response.
+
+ Args:
+ req: An request.
+
+ Return:
+ The response.
+ """
+ pkg = JSONPackage()
+ pkg.content = req
+ pkg.send_to(self._sock)
+ pkg.recv_from(self._sock)
+ return pkg.content
+
+ def close(self):
+ """Closes the socket."""
+ self._sock.close()
+
+
+def get_my_info(vim_info):
+ """Gets my information for server.
+
+ Args:
+ vim_info: An instance of VimInfo.
+
+ Return:
+ The information for server.
+ """
+ return {
+ _JSON_TOKEN.IDENTITY : vim_info.var.shared_vim_identity,
+ _JSON_TOKEN.INIT : vim_info.var.shared_vim_init,
+ _JSON_TOKEN.MODE : vim_info.mode,
+ _JSON_TOKEN.CURSORS : {
+ CURSOR_MARK.CURRENT : vim_info.cursors[CURSOR_MARK.CURRENT],
+ CURSOR_MARK.V : vim_info.cursors[CURSOR_MARK.V],
+ },
+ _JSON_TOKEN.TEXT : vim_info.text,
+ }
+
+
+def set_my_info(vim_info, json_info):
+ """Sets my information gived by server.
+
+ Args:
+ vim_info: An instance of VimInfo.
+ json_info: JSON information gived by server.
+ """
+ vim_info.text = json_info[_JSON_TOKEN.TEXT]
+ mode = json_info[_JSON_TOKEN.MODE]
+ vim_info.mode = mode
+ if mode in (MODE.VISUAL, MODE.BLOCK_VISUAL, MODE.LINE_VISUAL):
+ old_mode, vim_info.mode = mode, MODE.NORMAL
+ vim_info.cursors[CURSOR_MARK.V] = \
+ json_info[_JSON_TOKEN.CURSORS][CURSOR_MARK.V]
+ vim_info.mode = old_mode
+ vim_info.cursors[CURSOR_MARK.CURRENT] = \
+ json_info[_JSON_TOKEN.CURSORS][CURSOR_MARK.CURRENT]
+
+
+def set_others_info(vim_info, json_info):
+ """Sets the informations about other user.
+
+ Args:
+ vim_info: An instance of VimInfo.
+ json_info: JSON information gived by server.
+ """
+ users = json_info[_JSON_TOKEN.OTHERS]
+ vim_info.highlight.reset([user[_JSON_TOKEN.NICKNAME] for user in users])
+ for user in users:
+ name, mode = user[_JSON_TOKEN.NICKNAME], user[_JSON_TOKEN.MODE]
+ cursors = user[_JSON_TOKEN.CURSORS]
+ curr_rc = vim_info.num_to_rc(cursors[CURSOR_MARK.CURRENT])
+ vim_info.highlight[name].set_mode_cursor(mode, curr_rc)
+ if mode in (MODE.VISUAL, MODE.LINE_VISUAL, MODE.BLOCK_VISUAL):
+ last_rc = vim_info.num_to_rc(cursors[CURSOR_MARK.V])
+ if last_rc[0] > curr_rc[0] or \
+ (last_rc[0] == curr_rc[0] and last_rc[1] > curr_rc[1]):
+ last_rc, curr_rc = curr_rc, last_rc
+ set_other_visual(vim_info, name, mode, last_rc, curr_rc)
+ vim_info.highlight.render()
+
+def set_other_visual(vim_info, name, mode, beg, end):
+ """Sets the other user's visual block.
+
+ Args:
+ vim_info: An instance of VimInfo.
+ name: Name of this user.
+ mode: Mode of this user.
+ beg: The first row-column position of the range.
+ end: The last row-column position of the range.
+ """
+ if mode == MODE.VISUAL:
+ for row in range(beg[0], end[0] + 1):
+ first = 0 if row != beg[0] else beg[1]
+ last = vim_info.rowlen(row) if row != end[0] else end[1]
+ for col in range(first, last + 1):
+ vim_info.highlight[name].add_visual((row, col))
+ elif mode == MODE.LINE_VISUAL:
+ for row in range(beg[0], end[0] + 1):
+ for col in range(0, vim_info.rowlen(row)):
+ vim_info.highlight[name].add_visual((row, col))
+ elif mode == MODE.BLOCK_VISUAL:
+ left, right = min([beg[1], end[1]]), max([beg[1], end[1]])
+ for row in range(beg[0], end[0] + 1):
+ for col in range(left, right + 1):
+ vim_info.highlight[name].add_visual((row, col))
+
+
+class ServerError(Exception):
+ """Error raised by server."""
+ pass
+
+def main():
+ """Main function."""
+ try:
+ vim_info = VimInfo()
+ conn = TCPClient(vim_info)
+ response = conn.request(get_my_info(vim_info))
+ conn.close()
+ if _JSON_TOKEN.ERROR in response:
+ raise Exception(response[_JSON_TOKEN.ERROR])
+ set_my_info(vim_info, response)
+ set_others_info(vim_info, response)
+ except TCPClientError as e:
+ print(e)
+ except ServerError as e:
+ print(e)
+ except Exception as e:
+ import sys
+ print('?? %r' % e)
+
+main()
+EOF
+ let b:shared_vim_init = 0
+endfunction
+
+
+function! SharedVimEventsHandler(event_name)
+ if exists('b:shared_vim_server_name')
+ if a:event_name == 'CursorMoved'
+ call SharedVimSync()
+ elseif a:event_name == 'CursorMovedI'
+ call SharedVimSync()
+ elseif a:event_name == 'CursorHold'
+ call SharedVimSync()
+ elseif a:event_name == 'CursorHoldI'
+ call SharedVimSync()
+ elseif a:event_name == 'InsertEnter'
+ call SharedVimSync()
+ elseif a:event_name == 'InsertLeave'
+ call SharedVimSync()
+ endif
+ endif
+endfunction
+
+for i in range(1, 5)
+ exec 'hi SharedVimNor' . i . ' ctermbg=darkyellow'
+ exec 'hi SharedVimIns' . i . ' ctermbg=darkred'
+ exec 'hi SharedVimVbk' . i . ' ctermbg=darkblue'
+endfor
+
+autocmd! CursorMoved * call SharedVimEventsHandler('CursorMoved')
+autocmd! CursorMovedI * call SharedVimEventsHandler('CursorMovedI')
+autocmd! CursorHold * call SharedVimEventsHandler('CursorHold')
+autocmd! CursorHoldI * call SharedVimEventsHandler('CursorHoldI')
+autocmd! InsertEnter * call SharedVimEventsHandler('InsertEnter')
+autocmd! InsertLeave * call SharedVimEventsHandler('InsertLeave')
+