diff options
author | cathook <b01902109@csie.ntu.edu.tw> | 2014-11-09 02:40:08 +0800 |
---|---|---|
committer | cathook <b01902109@csie.ntu.edu.tw> | 2014-11-09 02:40:08 +0800 |
commit | fd1fbcecb1ef75fe8016ab909ae4092ca705b2f1 (patch) | |
tree | 252f682763ec8356bc01ebf8c611dcaa0a07515f | |
parent | 3f84cb8b7c2aa24e739ae1a02c883e1e4cdb4004 (diff) | |
download | vim-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/Makefile | 12 | ||||
-rw-r--r-- | server/setup.py | 17 | ||||
-rw-r--r-- | server/src/.gitignore | 3 | ||||
-rw-r--r-- | server/src/authority_string_transformer.py | 42 | ||||
-rw-r--r-- | server/src/cmd_ui.py | 227 | ||||
-rw-r--r-- | server/src/json_package.py | 114 | ||||
-rw-r--r-- | server/src/log.py | 33 | ||||
-rwxr-xr-x | server/src/shared_vim_server.py | 119 | ||||
-rw-r--r-- | server/src/tcp_server.py | 254 | ||||
-rw-r--r-- | server/src/text_chain.py | 700 | ||||
-rw-r--r-- | server/src/users_text_manager.py | 169 | ||||
-rw-r--r-- | test_tool/.gitignore | 3 | ||||
-rwxr-xr-x | test_tool/fake_server.py | 89 | ||||
l--------- | test_tool/json_package.py | 1 | ||||
l--------- | test_tool/log.py | 1 | ||||
-rwxr-xr-x | test_tool/ping_server.py | 62 | ||||
l--------- | test_tool/tcp_server.py | 1 | ||||
l--------- | test_tool/users_text_manager.py | 1 | ||||
-rw-r--r-- | vim/plugin/shared_vim.vim | 778 |
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') + |