diff options
-rw-r--r-- | server/src/authority_string_transformer.py | 72 | ||||
-rw-r--r-- | server/src/cmd_ui.py | 159 | ||||
-rw-r--r-- | server/src/json_package.py | 129 | ||||
-rw-r--r-- | server/src/request_handler.py | 142 | ||||
-rwxr-xr-x | server/src/shared_vim_server.py | 5 | ||||
-rw-r--r-- | server/src/tcp_server.py | 191 | ||||
-rw-r--r-- | server/src/text_chain.py | 89 | ||||
-rw-r--r-- | server/src/users_text_manager.py | 71 | ||||
-rw-r--r-- | vim/plugin/shared_vim.vim | 575 |
9 files changed, 669 insertions, 764 deletions
diff --git a/server/src/authority_string_transformer.py b/server/src/authority_string_transformer.py index 8db484b..f99daa1 100644 --- a/server/src/authority_string_transformer.py +++ b/server/src/authority_string_transformer.py @@ -1,42 +1,44 @@ -"""AuthroityStringTransformer.""" +"""For transformating the type of authroity.""" from users_text_manager import AUTHORITY -class AuthorityStringTransformerError(Exception): +_mappings = [ + (AUTHORITY.READONLY, 'RO'), + (AUTHORITY.READWRITE, 'RW'), +] + + +class Error(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.') + +def to_string(authority): + """Transform number authority value to string value. + + Args: + authority: Authority in number format. + + Return: + Corrosponding authority in string format. + """ + for item in _mappings: + if item[0] == authority: + return item[1] + raise Error('Invalid number.') + + +def to_number(string): + """Transform string authority value to number value. + + Args: + authority: Authority in string format. + + Return: + Corrosponding authority in number format. + """ + for item in _mappings: + if item[1] == string: + return item[0] + raise Error('Invalid string.') diff --git a/server/src/cmd_ui.py b/server/src/cmd_ui.py index e45eb70..b7079da 100644 --- a/server/src/cmd_ui.py +++ b/server/src/cmd_ui.py @@ -1,33 +1,43 @@ -"""Command line user interface.""" +"""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 UNKNOWN -from users_text_manager import UsersTextManagerError +import authority_string_transformer -INTRO = '' +INTRO = 'Type the command "help" for help document.' PROMPT = '> ' class CmdUI(cmd.Cmd): # pylint: disable=R0904 """Command line user interface. + It's a simple UI for doing operation on server, supplied commands: + - Add/delete/reset a user + - List (online) users + - Save/load the user list to/from a file. + - Exit. + - Prints the help document. + + And it is also the main output interface of the whole program. + 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. + _init_cmds: Initialize commands. """ - def __init__(self, users_text_manager, tcp_server, shared_vim_server): + def __init__(self, + init_cmds, users_text_manager, tcp_server, shared_vim_server): """Constructor. Args: + init_cmds: Lists of commands to run after startup. users_text_manager: An instance of UsersTextManager. + tcp_server: An instance of TCPServer. shared_vim_server: An instance of SharedVimServer. """ super(CmdUI, self).__init__(INTRO) @@ -37,126 +47,113 @@ class CmdUI(cmd.Cmd): # pylint: disable=R0904 self._shared_vim_server = shared_vim_server self._stop_flag = False self._thread = None + self._init_cmds = init_cmds 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) + if identity in self._users_text_manager.get_users_info(): + self.write('The identity %r is already in used.\n' % identity) + return + authority = authority_string_transformer.to_number(authority_str) self._users_text_manager.add_user(identity, nickname, authority) - self.write('Done\n') + user_info = self._users_text_manager.get_users_info()[identity] + self.write('Added %s => %s\n' % (identity, str(user_info))) 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: + except authority_string_transformer.Error 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] + if identity not in self._users_text_manager.get_users_info(): + self.write('The identity %r is not in used.\n' % identity) + return 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(): + for identity in self._users_text_manager.get_users_info(): 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_reset(self, text): """Resets a user, [usage] reset <identity>""" try: iden = _split_text(text, 1)[0] if iden not in self._users_text_manager.get_users_info(): - self.write('Fail: Identity %r not found\n' % iden) - else: - self._users_text_manager.reset_user(iden) - self.write('Done\n') + self.write('The User with identity %r is not exist.\n' % iden) + return + self._users_text_manager.reset_user(iden) + user_info = self._users_text_manager.get_users_info()[iden] + self.write('Reseted %s ==> %s\n' % (iden, str(user_info))) except _SplitTextError: self.write('Format error!\n' + '[usage] reset <identity>\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)) + infos = self._users_text_manager.get_users_info().items() + for iden, user in sorted(infos, key=lambda x: x[0]): + self.write('%-10s => %s' % (iden, str(user))) except _SplitTextError: self.write('Format error!\n' + '[usage] list\n') - except UsersTextManagerError as e: - self.write('Fail: %r\n' % e) def do_online(self, text): """Lists online users, [usage] online""" try: _split_text(text, 0) - for iden, user in self._users_text_manager.get_users_info().items(): - if user.mode == UNKNOWN: - continue - self.write('%r => %s' % (iden, user)) + infos = self._users_text_manager.get_users_info( + must_online=True).items() + for iden, user in sorted(infos, key=lambda x: x[0]): + self.write('%-10s => %s' % (iden, str(user))) except _SplitTextError: self.write('Format error!\n' + '[usage] online\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] + for line in f.readlines(): + line = line if not line.endswith('\n') else 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) + continue + self.do_add(line) 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)) + self.write('Error occured when opening the file: %r' % 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)) + users_info = self._users_text_manager.get_users_info().items() + for iden, user in sorted(users_info, key=lambda x: x[0]): + auth_str = authority_string_transformer.to_string( + user.authority) + f.write('%s %s %s\n' % (iden, user.nick_name, auth_str)) except _SplitTextError: self.write('Format error!\n' + '[usage] save <filename>\n') @@ -165,12 +162,21 @@ class CmdUI(cmd.Cmd): # pylint: disable=R0904 def do_port(self, text): """Print the server's port.""" - _split_text(text, 0) - self.write('server port = %r' % self._tcp_server.port) + try: + _split_text(text, 0) + self.write('Server port = %r\n' % self._tcp_server.port) + except _SplitTextError: + self.write('Format error!\n' + + '[usage] port\n') def do_exit(self, text): """Exits the program.""" - self._shared_vim_server.stop() + try: + _split_text(text, 0) + self._shared_vim_server.stop() + except _SplitTextError: + self.write('Format error!\n' + '[usage] exit\n') def do_echo(self, text): # pylint: disable=R0201 """Echo.""" @@ -178,9 +184,13 @@ class CmdUI(cmd.Cmd): # pylint: disable=R0904 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)) + try: + _split_text(text, 0) + commands = [m[3 : ] for m in dir(self) if m.startswith('do_')] + self.write('Commands: \n' + ' '.join(commands)) + except _SplitTextError: + self.write('Format error!\n' + + '[usage] help\n') def do_EOF(self, text): # pylint: disable=C0103 """Same as exit""" @@ -202,25 +212,18 @@ class CmdUI(cmd.Cmd): # pylint: disable=R0904 Args: text: String to be printed. """ - self.onecmd('echo ' + text) + for line in text.splitlines(): + self.onecmd('echo ' + line) - def start(self, init_cmds=None): - """Starts this CmdUI. + def preloop(self): + for c in self._init_cmds: + self.onecmd(c) - 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,)) + def start(self): + """Starts this CmdUI.""" + self._thread = threading.Thread(target=self.cmdloop, + args=(INTRO,)) self._thread.start() - for c in init_cmds if init_cmds else []: - self.onecmd(c) def stop(self): """Stops the command line UI.""" @@ -245,10 +248,10 @@ def _split_text(text, num): Args: text: The string to be splitted. - num: Length of the tuple. + num: Number of elements in the result tuple. Return: - A num-tuple. + A <num>-tuple. """ words = [word for word in re.split(r'[ \t]', text) if word] if len(words) != num: diff --git a/server/src/json_package.py b/server/src/json_package.py index f01e489..a4b4b5e 100644 --- a/server/src/json_package.py +++ b/server/src/json_package.py @@ -1,7 +1,6 @@ -"""Contains tcp package object.""" +"""JSONPackage""" import json -import zlib class JSONPackageError(Exception): @@ -9,106 +8,68 @@ class JSONPackageError(Exception): pass class JSONPackage(object): - """Send/receive json by tcp connection. + """Send/receive json object by gived function. - Attribute: + Attributes: 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. + Static attributes: + _ENCODING: Encoding of the package. + _HEADER_LENGTH: Length of the header. + """ + _ENCODING = 'utf-8' + _HEADER_LENGTH = 10 + def __init__(self, content=None, recv_func=None): + """Constructor. - Args: - string: Data string. + If the receive_func is not None, it will grap the default content by + calling that function instead of by the argument "content". - 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. + The detail of arguments/return values format see the method "recv_from". Args: - body: Package body. - - Returns: - Package header. + content: The default content of this package. + recv_func: A function for receive the default content. """ - header_string = ('%%0%dd' % JSONPackage.HEADER_LENGTH) % len(body) - return header_string.encode(JSONPackage.ENCODING) + self.content = content + if recv_func is not None: + self.recv(recv_func) - @staticmethod - def _recv_header_string(conn): - """Receives package header from specified tcp connection. + def send(self, send_func): + """Sends by calling the gived sending function. Args: - conn: The specified tcp connection. - - Returns: - Package header. + send_func: A function which will send the whole data gived. + Function format: + send_func(bytes_data): None """ try: - byte = conn.recv(JSONPackage.HEADER_LENGTH) - return byte.decode(JSONPackage.ENCODING) + body = bytes(json.dumps(self.content), JSONPackage._ENCODING) + header_str = ('%%0%dd' % JSONPackage._HEADER_LENGTH) % len(body) + send_func(bytes(header_str, JSONPackage._ENCODING) + body) + except TypeError as e: + raise JSONPackageError('json: %r' % e) except UnicodeError as e: - raise JSONPackageError('Cannot decode the header string: %r.' % e) + raise JSONPackageError('Cannot encode the string: %r.' % e) - @staticmethod - def _recv_body_string(conn, header): - """Receives package body from specified tcp connection and header. + def recv(self, recv_func): + """Receives a json object from a gived function. - Args: - conn: The specified tcp connection. - header: The package header. + It will calls the give function like this: + recv_func(<num_of_bytes>) => bytes with length <num_of_bytes> - Returns: - Package body. + Args: + recv_func: A function to be called to get the serialize data. """ try: - body_length = int(header) - body = conn.recv(body_length) - body_byte = zlib.decompress(body) - return body_byte.decode(JSONPackage.ENCODING) + header_str = str(recv_func(JSONPackage._HEADER_LENGTH), + JSONPackage._ENCODING) + body_str = str(recv_func(int(header_str)), JSONPackage._ENCODING) except UnicodeError as e: - raise JSONPackageError('Cannot decode the body string: %r.' % e) + raise JSONPackageError('Cannot decode the bytes: %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) + raise JSONPackageError('Cannot get the body length %r' % e) + try: + self.content = json.loads(body_str) + except ValueError as e: + raise JSONPackageError('Cannot loads to the json object: %r' % e) diff --git a/server/src/request_handler.py b/server/src/request_handler.py new file mode 100644 index 0000000..9d820e4 --- /dev/null +++ b/server/src/request_handler.py @@ -0,0 +1,142 @@ +"""RequestHandler.""" + +import log + +from users_text_manager import AUTHORITY +from users_text_manager import UserInfo + + +class JSON_TOKEN: # pylint:disable=W0232 + """Enumeration the Ttken strings for json object.""" + BYE = 'bye' # Resets the user and do nothong. + 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 RequestHandler(object): + """Handles all kinds of request. + + Attributes: + _users_text_manager: An instance of UsersTextManager. + """ + def __init__(self, users_text_manager): + """Constructor. + + Args: + users_text_manager: An instance of UsersTextManager. + """ + super(RequestHandler, self).__init__() + self._users_text_manager = users_text_manager + + def handle(self, request): + """Handles the request and returns the response. + + Args: + request: The request. + + Return + The respsonse. + """ + if JSON_TOKEN.IDENTITY not in request: + return {JSON_TOKEN.ERROR : 'Bad request.'} + identity = request[JSON_TOKEN.IDENTITY] + if identity not in self._users_text_manager.get_users_info(): + return {JSON_TOKEN.ERROR: 'Invalid identity.'} + for handler in [self._try_handle_leave, + self._try_handle_sync]: + response = handler(identity, request) + if response is not None: + break + else: + return {JSON_TOKEN.ERROR: 'Bad request.'} + return response + + def _try_handle_leave(self, identity, request): + """Trying to handle the leaving operation if it is. + + Args: + identity: The identity of that user. + request: The request from that user. + """ + if JSON_TOKEN.BYE in request: + self._users_text_manager.reset_user(identity) + return {} + + def _try_handle_sync(self, identity, request): + """Trying to handle the sync request if it is, otherwise return None. + + Args: + identity: The identity of that user. + request: The request from that user. + """ + if all(key in request for key in [JSON_TOKEN.INIT, + JSON_TOKEN.TEXT, + JSON_TOKEN.MODE, + JSON_TOKEN.CURSORS]): + log.info('handle sync-request from %r\n' % identity) + self._check_init(identity, request) + self._check_authority(identity, request) + new_user_info, new_text = self._users_text_manager.update_user_text( + identity, + UserInfo(mode=request[JSON_TOKEN.MODE], + cursors=request[JSON_TOKEN.CURSORS]), + request[JSON_TOKEN.TEXT]) + return self._pack_sync_response(identity, new_user_info, new_text) + + def _pack_sync_response(self, identity, user_info, text): + """Packs the response for the sync request by the result from manager. + + Args: + identity: Identity of that user. + user_info: Informations of that user. + text: New text. + + Return: + The response json object. + """ + return {JSON_TOKEN.TEXT : text, + JSON_TOKEN.CURSORS : user_info.cursors, + JSON_TOKEN.MODE : 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() + ]} + + def _check_init(self, identity, request): + """Checks whether that user should be initialize or not. + + If yes, it will reset that user and update the request. + + Args: + identity: The identity of that user. + request: The request from that user. + """ + if request[JSON_TOKEN.INIT]: + log.info('Init the user %r\n' % identity) + self._users_text_manager.reset_user(identity) + request[JSON_TOKEN.TEXT] = '' + for mark in request.get(JSON_TOKEN.CURSORS, []): + request[JSON_TOKEN.CURSORS][mark] = 0 + + def _check_authority(self, identity, request): + """Checks the authroity and updates the request. + + If the user is not writeable, it will modify the request to let it looks + like that the user did nothing. + + Args: + identity: The identity of that user. + request: The request from that user. + """ + 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 diff --git a/server/src/shared_vim_server.py b/server/src/shared_vim_server.py index a7f5201..9ec7fcd 100755 --- a/server/src/shared_vim_server.py +++ b/server/src/shared_vim_server.py @@ -57,14 +57,15 @@ class SharedVimServer(threading.Thread): 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) + self._cmd_ui = CmdUI(['load %s' % self._args.user_list_filename], + 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.start() self._cmd_ui.join() self._tcp_server.join() diff --git a/server/src/tcp_server.py b/server/src/tcp_server.py index 28bf0fc..7060244 100644 --- a/server/src/tcp_server.py +++ b/server/src/tcp_server.py @@ -8,26 +8,11 @@ import time from json_package import JSONPackage from json_package import JSONPackageError -from users_text_manager import AUTHORITY -from users_text_manager import UserInfo +from request_handler import RequestHandler FREQUENCY = 8 - -TIMEOUT = 5 - - -class _JSON_TOKEN: # pylint:disable=W0232 - """Enumeration the Ttken strings for json object.""" - BYE = 'bye' # Resets the user and do nothong. - 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 +TIMEOUT = 1 class TCPServer(threading.Thread): @@ -68,6 +53,7 @@ class TCPServer(threading.Thread): """Stops the thread.""" self._stop_flag = True for thr in self._connection_handler_threads: + thr.stop() thr.join() def _build(self): @@ -103,11 +89,53 @@ class TCPServer(threading.Thread): self._connection_handler_threads += [thr] +class _TCPConnectionHandler(threading.Thread): + """A thread to handle a connection. + + Attributes: + _sock: The connection socket. + _users_text_manager: An instance of UsersTextManager. + _stop_flag: Stopping flag. + """ + def __init__(self, conn, users_text_manager): + """Constructor. + + Args: + conn: The connection. + users_text_manager: An instance of UsersTextManager. + """ + super(_TCPConnectionHandler, self).__init__() + self._conn = TCPConnection(conn) + self._users_text_manager = users_text_manager + self._stop_flag = False + self._request_handler = RequestHandler(self._users_text_manager) + + def run(self): + """Runs the thread.""" + try: + while not self._stop_flag: + try: + request = JSONPackage(recv_func=self._conn.recv_all).content + response = self._request_handler.handle(request) + JSONPackage(response).send(self._conn.send_all) + except JSONPackageError as e: + log.error(str(e)) + except socket.error as e: + log.error(str(e)) + self._conn.close() + + def stop(self): + """Stops the thread.""" + self._stop_flag = True + self._conn.stop() + + class TCPConnection(object): """My custom tcp connection. Args: _conn: The TCP-connection. + _stop_flag: Stopping flag. """ def __init__(self, conn): """Constructor. @@ -117,16 +145,22 @@ class TCPConnection(object): """ self._conn = conn self._conn.settimeout(TIMEOUT) + self._stop_flag = False - def send(self, data): + def send_all(self, data): """Sends the data until timeout or the socket closed. Args: data: Data to be sent. """ - self._conn.sendall(data) + recvd_byte, total_byte = 0, len(data) + while recvd_byte < total_byte and not self._stop_flag: + try: + recvd_byte += self._conn.send(data[recvd_byte : ]) + except socket.timeout: + continue - def recv(self, nbyte): + def recv_all(self, nbyte): """Receives the data until timeout or the socket closed. Args: @@ -136,8 +170,11 @@ class TCPConnection(object): Bytes of data. """ ret = b'' - while nbyte > 0: - recv = self._conn.recv(nbyte) + while nbyte > 0 and not self._stop_flag: + try: + recv = self._conn.recv(nbyte) + except socket.timeout: + continue if not recv: raise socket.error('Connection die.') ret += recv @@ -148,110 +185,6 @@ class TCPConnection(object): """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.get(_JSON_TOKEN.INIT) or request.get(_JSON_TOKEN.BYE, False): - self._users_text_manager.reset_user(identity) - request[_JSON_TOKEN.TEXT] = '' - for mark in request.get(_JSON_TOKEN.CURSORS, []): - request[_JSON_TOKEN.CURSORS][mark] = 0 - if _JSON_TOKEN.BYE in request: - return None - 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) + def stop(self): + """Stops.""" + self._stop_flag = True diff --git a/server/src/text_chain.py b/server/src/text_chain.py index 59d65c2..d60ea02 100644 --- a/server/src/text_chain.py +++ b/server/src/text_chain.py @@ -47,13 +47,11 @@ class TextChain(object): 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 : ]]) + commit.apply_commits([cm[1] for cm 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 : ]]) + info.apply_commits([cm[1] for cm 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) @@ -217,89 +215,6 @@ class _TextCommit(object): 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. diff --git a/server/src/users_text_manager.py b/server/src/users_text_manager.py index e21b382..93d9d94 100644 --- a/server/src/users_text_manager.py +++ b/server/src/users_text_manager.py @@ -4,6 +4,7 @@ import threading from text_chain import TextChain + UNKNOWN = -1 class AUTHORITY: # pylint:disable=W0232 @@ -13,16 +14,17 @@ class AUTHORITY: # pylint:disable=W0232 class UserInfo(object): - """A pure structor for represent a user's information. + """A structure for storing 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. + cursors: A dict stores cursor positions of each mark. + last_commit_id: Last commit's id. """ - def __init__(self, authority=UNKNOWN, nick_name=''): + def __init__(self, authority=UNKNOWN, nick_name='', mode=UNKNOWN, + cursors=None): """Constructor. Args: @@ -31,18 +33,14 @@ class UserInfo(object): """ self.authority = authority self.nick_name = nick_name - self.mode = UNKNOWN - self.cursors = {} + self.mode = mode + self.cursors = {} if cursors is None else 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) - + return 'authorith = %r, nickname = %r, mode = %r, last_commit = %r' % ( + self.authority, self.nick_name, self.mode, self.last_commit_id) -class UsersTextManagerError(Exception): - """Error raised by UsersTextManager.""" - pass class UsersTextManager(object): """Handles query/operations about users and texts. @@ -77,12 +75,8 @@ class UsersTextManager(object): 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 + self._users[identity] = UserInfo(authority, nick_name) + self._users[identity].last_commit_id = self._text_chain.new() def delete_user(self, identity): """Deletes a user. @@ -91,8 +85,6 @@ class UsersTextManager(object): 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] @@ -136,23 +128,20 @@ class UsersTextManager(object): 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))] + curmarks = new_user_info.cursors.keys() + curs = [new_user_info.cursors[mark] for mark in curmarks] + new_commit_id, new_text, new_curs = self._text_chain.commit( + self._users[identity].last_commit_id, new_text, curs) self._users[identity].last_commit_id = new_commit_id self._users[identity].mode = new_user_info.mode - self._users[identity].cursors = dict(curlist) + self._users[identity].cursors = _lists_to_dict(curmarks, new_curs) 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))]) + curmarks = user.cursors.keys() + curs = [user.cursors[mark] for mark in curmarks] + new_curs = self._text_chain.update_cursors(curs) + user.cursors = _lists_to_dict(curmarks, new_curs) return (self._users[identity], new_text) def get_user_text(self, identity): @@ -165,5 +154,19 @@ class UsersTextManager(object): The text. """ with self._rlock: - return self._text_chain.get_text(self._users[identity]. - last_commit_id) + return self._text_chain.get_text( + self._users[identity].last_commit_id) + + +def _lists_to_dict(list1, list2): + """Combines each element in two lists. + + Args: + list1: The first list. + list2: The second list. + + Return: + A list with each element be a tuple of two element in list1 and list2. + """ + l1, l2 = list(list1), list(list2) + return {l1[index] : l2[index] for index in range(len(l1))} diff --git a/vim/plugin/shared_vim.vim b/vim/plugin/shared_vim.vim index 085908f..27e825c 100644 --- a/vim/plugin/shared_vim.vim +++ b/vim/plugin/shared_vim.vim @@ -2,19 +2,11 @@ command! -nargs=0 SharedVimTryUsePython3 call _SharedVimTryUsePython3(1) command! -nargs=0 SharedVimTryUsePython2 call _SharedVimTryUsePython2(1) command! -nargs=+ SharedVimConnect call _SharedVimCallPythonFunc('connect', [<f-args>]) -command! -nargs=0 SharedVimDisconnect call _SharedVimCallPythonFunc('disconnect', [<f-args>]) -command! -nargs=0 SharedVimSync call _SharedVimCallPythonFunc('sync', [<f-args>]) -command! -nargs=0 SharedVimShowInfo call _SharedVimCallPythonFunc('show_info', [<f-args>]) - - -"""""""""""""""""""""""""" Global variable for settings """""""""""""""""""""""" -if !exists('g:shared_vim_timeout') - let g:shared_vim_timeout = 5 -endif - -if !exists('g:shared_vim_num_groups') - let g:shared_vim_num_groups = 5 -endif +command! -nargs=0 SharedVimDisconnect call _SharedVimCallPythonFunc('disconnect', []) +command! -nargs=0 SharedVimSync call _SharedVimCallPythonFunc('sync', []) +command! -nargs=0 SharedVimShowInfo call _SharedVimCallPythonFunc('show_info', []) +command! -nargs=1 SharedVimSetTimeout call _SharedVimCallPythonFunc('set_bvar', ['TIMEOUT', <f-args>]) +command! -nargs=1 SharedVimSetNumOfGroups call _SharedVimCallPythonFunc('set_bvar', ['NUM_GROUPS', <f-args>]) """"""""""""""""""""""""""""""""""""" Setup """""""""""""""""""""""""""""""""""" @@ -26,13 +18,16 @@ for i in range(0, 100) endfor +let g:shared_vim_auto_sync_level = 3 + + " Auto commands -autocmd! CursorMoved * SharedVimSync -autocmd! CursorMovedI * SharedVimSync -autocmd! CursorHold * SharedVimSync -autocmd! CursorHoldI * SharedVimSync -autocmd! InsertEnter * SharedVimSync -autocmd! InsertLeave * SharedVimSync +autocmd! InsertLeave * call _SharedVimAutoSync(1) +autocmd! CursorMoved * call _SharedVimAutoSync(1) +autocmd! CursorHold * call _SharedVimAutoSync(1) +autocmd! InsertEnter * call _SharedVimAutoSync(2) +autocmd! CursorMovedI * call _SharedVimAutoSync(3) +autocmd! CursorHoldI * call _SharedVimAutoSync(3) """"""""""""""""""""""""""""""""""" Functions """""""""""""""""""""""""""""""""" @@ -73,7 +68,16 @@ function! _SharedVimCallPythonFunc(func_name, args) exe 'SharedVimPython ' . a:func_name . '(' . args_str . ')' endif endif -endfunctio +endfunction + +function! _SharedVimAutoSync(level) + if !exists('b:shared_vim_auto_sync_level') + let b:shared_vim_auto_sync_level = g:shared_vim_auto_sync_level + endif + if a:level <= b:shared_vim_auto_sync_level + SharedVimSync + endif +endfunction function! _SharedVimSetup() @@ -86,16 +90,10 @@ import re import socket import sys import vim -import zlib if sys.version_info[0] == 3: unicode = str -class GOALS: # pylint:disable=W0232 - SYNC = 'sync' # Sync. - DISCONNECT = 'disconnect' # Disconnect. - SHOW_INFO = 'show_info' # Shows the users. - class CURSOR_MARK: # pylint:disable=W0232 """Enumeration type of cursor marks.""" CURRENT = '.' @@ -118,15 +116,14 @@ class MODE: # pylint:disable=W0232 class VARNAMES: # pylint: disable=W0232 """Enumeration types of variable name in vim.""" - PREFIX = 'shared_vim_' # Shared prefix of the variable names. - IDENTITY = PREFIX + 'identity' # Identity of the user. - INIT = PREFIX + 'init' # Initial or not. - NUM_GROUPS = PREFIX + 'num_groups' # Number of groups. - SERVER_NAME = PREFIX + 'server_name' # Server name. - SERVER_PORT = PREFIX + 'port' # Server port. - TIMEOUT = PREFIX + 'timeout' # Timeout for TCPConnection. - USERS = PREFIX + 'users' # List of users. - GOAL = PREFIX + 'goal' # Goal of this python code. + GROUP_NAME_PREFIX = 'gnp' # Group name's prefix. + IDENTITY = 'identity' # Identity of the user. + INIT = 'init' # Initial or not. + NUM_GROUPS = 'num_groups' # Number of groups. + SERVER_NAME = 'server_name' # Server name. + SERVER_PORT = 'port' # Server port. + TIMEOUT = 'timeout' # Timeout for TCPConnection. + USERS = 'users' # List of users. # Name of the normal cursor group. NORMAL_CURSOR_GROUP = lambda x: 'SharedVimNor%d' % x @@ -137,9 +134,11 @@ INSERT_CURSOR_GROUP = lambda x: 'SharedVimIns%d' % x # Name of the visual group. VISUAL_GROUP = lambda x: 'SharedVimVbk%d' % x -DEFAULT_TIMEOUT = 5 + +DEFAULT_TIMEOUT = 1 DEFAULT_NUM_GROUPS = 5 + class JSON_TOKEN: # pylint:disable=W0232 """Enumeration the Ttken strings for json object.""" BYE = 'bye' # Disconnects with the server. @@ -153,255 +152,89 @@ class JSON_TOKEN: # pylint:disable=W0232 TEXT = 'text' # text content in the buffer -if hasattr(vim, 'options'): - vim_options = vim.options -else: - class SimpleVimOptions(object): - """An alternative implement of vim.options when it is not exists.""" - def __getitem__(self, option_name): - """Gets the specified vim option. - - Args: - option_name: Name of the option to get. - - Return: - The value in string. - """ - return vim.eval('&' + option_name) - - vim_options = SimpleVimOptions() - - -if not hasattr(vim, 'vars') or not hasattr(vim.current.buffer, 'vars') or True: - class SimpleVimVars(object): - """An alternative implement of vim.vars/vim.current.buffer.vars. - - Attributes: - _prefix: The prefix of the variable name. For example, "b:" for - buffer's variable, "g:" for global variable. - """ - _STRING_NOTATION = 's' - _NUMBER_NOTATION = 'n' - - def __init__(self, prefix): - """Constructor. - - Args: - prefix: The prefix of the variable name. - """ - self._prefix = prefix - - def __getitem__(self, variable_name): - """Gets the specified vim variable. - - Args: - variable_name: Name of the variable to get. - - Return: - The value. - """ - value = vim.eval(self._prefix + variable_name) - if value.startswith(self._STRING_NOTATION): - return value[1 : ] - else: - return int(value[1 : ]) - - def __setitem__(self, variable_name, value): - """Sets the specifiec vim variable. - - Args: - variable_name: Name of the variable to set. - value: The new value. - """ - if isinstance(value, bytes): - value = self._STRING_NOTATION + value - else: - value = '%s%d' % (self._NUMBER_NOTATION, value) - vim.command('let %s%s = "%s"' % - (self._prefix, variable_name, value)) - - def __delitem__(self, variable_name): - """Deletes the specifiec vim variable. - - Args: - variable_name: Name of the variable to delete. - """ - vim.command('unlet %s%s' % (self._prefix, variable_name)) - - def __contains__(self, variable_name): - """Checks whether the variable is exist or not. - - Args: - variable_name: Name of the variable to check. +############### Handler for Variable stores only in python ##################### +class ScopeVars(object): + """A scoped variables handler. - Return: - True if the variable exists; otherwise, False. - """ - return vim.eval('exists("%s%s")' % - (self._prefix, variable_name)) == '1' - - -class JSONPackage(object): - """Send/receive json by tcp connection. - - Attribute: - content: Content of the package body. + Attributes: + _curr_scope: Current scope number. + _vars: A dict contains all scope's variables and the values. """ - 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 variables in vim. + self._curr_scope = None + self._vars = {} - Attributes: - _getter: A function which will return the object for this class to - access this variablies. - """ - def __init__(self, var=None, getter=None): - """Constructor. + @property + def curr_scope(self): + """Gets the current scope number.""" + return self._curr_scope - Args: - var: The object for this class to access the vars. - getter: If it is not None, it must be a function which will return - the object for this class to access the vars. - """ - if getter is None: - self._getter = lambda : var - else: - self._getter = getter + @curr_scope.setter + def curr_scope(self, value): + """Sets the current scope number.""" + self._curr_scope = value + self._vars.setdefault(self._curr_scope, {}) - def __getitem__(self, variable_name): - """Gets the specified vim variable. + def get(self, variable_name, default=None): + """Gets the specified variable. Args: variable_name: Name of the variable to get. + default: The default value to return if the variable is not exist. Return: - None if the value is not exists, otherwise the value. + The value. """ - return self.get(variable_name) + return self._vars[self._curr_scope].get(variable_name, default) - def get(self, variable_name, default_value=None): - """Gets the specified vim variable. + def __getitem__(self, variable_name): + """Gets the specified variable. Args: variable_name: Name of the variable to get. - default_value: The default to return if the variable is not exist. Return: - default_value if the value is not exists, otherwise the value. + The value. """ - if variable_name not in self._getter(): - return default_value - return VimInfo.transform_to_py(self._getter()[variable_name]) + return self._vars[self._curr_scope][variable_name] def __setitem__(self, variable_name, value): - """Sets the specifiec vim variable. + """Sets the specifiec variable. Args: variable_name: Name of the variable to set. value: The new value. """ - self._getter()[variable_name] = VimInfo.transform_to_vim(value) + self._vars[self._curr_scope][variable_name] = value def __delitem__(self, variable_name): - """Deletes the specifiec vim variable. + """Deletes the specifiec variable. Args: variable_name: Name of the variable to delete. """ - del self._getter()[variable_name] + del self._vars[self._curr_scope][variable_name] def __contains__(self, variable_name): """Checks whether the variable is exist or not. Args: variable_name: Name of the variable to check. + + Return: + True if the variable exists; otherwise, False. """ - return variable_name in self._getter() + return variable_name in self._vars[self._curr_scope] + + +# For scope=buffer +py_bvars = ScopeVars() +# For scope=window +py_wvars = ScopeVars() +############################### Interface to vim ############################### class VimCursorsInfo(object): # pylint: disable=W0232 """Gets/sets the cursor position in vim.""" def __init__(self, *_): @@ -596,32 +429,22 @@ class VimHighlightInfo(object): @staticmethod def num_of_groups(): """Gets the number of groups.""" - return VimInfo.bvar.get(VARNAMES.NUM_GROUPS, DEFAULT_TIMEOUT) + return py_bvars.get(VARNAMES.NUM_GROUPS, DEFAULT_NUM_GROUPS) class VimInfoMeta(type): """An interface for accessing the vim's vars, buffer, cursors, etc. Static attributes: - gvar: An instance of VimVarInfo, for accessing the global variables in - vim. - bvar: An instance of VimVarInfo, for accessing the buffer's variables in - vim. cursors: An instance of VimCursorsInfo, for accessing the cursor information in vim. highlight: An instance of VimHighlightInfo, for accessing the information about highlight in vim. ENCODING: vim's encoding. """ - if 'vars' in dir(vim): - gvar = VimVarInfo(vim.vars) - bvar = VimVarInfo(getter=lambda : vim.current.buffer.vars) - else: - gvar = VimVarInfo(SimpleVimVars('g:')) - bvar = VimVarInfo(SimpleVimVars('b:')) cursors = VimCursorsInfo() highlight = VimHighlightInfo() - ENCODING = vim_options['encoding'] + ENCODING = vim.eval('&encoding') def __init__(self, *args): """Constructor.""" @@ -630,14 +453,13 @@ class VimInfoMeta(type): @property def lines(self): # pylint: disable=R0201 """Gets list of lines in the buffer.""" - return [VimInfo.transform_to_py(line) - for line in vim.current.buffer[:]] + return [VimInfo.transform_to_py(line) for line in vim.current.buffer[:]] @lines.setter def lines(self, lines): # pylint: disable=R0201 """Sets the buffer by list of lines.""" - vim.current.buffer[0 : len(vim.current.buffer)] = \ - [VimInfo.transform_to_vim(line) for line in lines] + tr = [VimInfo.transform_to_vim(line) for line in lines] + vim.current.buffer[0 : len(vim.current.buffer)] = tr @property def text(self): @@ -696,17 +518,17 @@ class VimInfoMeta(type): priority: Priority for the vim function matchadd(). positions: List of row-column position. """ - last_id = VimInfo.bvar[VARNAMES.PREFIX + group_name] + last_id = py_wvars.get(VARNAMES.GROUP_NAME_PREFIX + group_name, None) if last_id is not None and last_id > 0: - ret = vim.eval('matchdelete(%d)' % last_id) - del VimInfo.bvar[VARNAMES.PREFIX + group_name] + vim.eval('matchdelete(%d)' % last_id) + del py_wvars[VARNAMES.GROUP_NAME_PREFIX + 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: - VimInfo.bvar[VARNAMES.PREFIX + group_name] = mid + py_wvars[VARNAMES.GROUP_NAME_PREFIX + group_name] = mid @staticmethod def transform_to_py(data): @@ -740,13 +562,13 @@ class VimInfoMeta(type): # Copy from https://bitbucket.org/gutworth/six/src/c17477e81e482d34bf3cda043b2eca643084e5fd/six.py def with_metaclass(meta, *bases): """Create a base class with a metaclass.""" - class metaclass(meta): + class metaclass(meta): # pylint: disable=W0232 def __new__(cls, name, this_bases, d): return meta(name, bases, d) return type.__new__(metaclass, 'temporary_class', (), {}) -class VimInfo(with_metaclass(VimInfoMeta, object)): +class VimInfo(with_metaclass(VimInfoMeta, object)): # pylint: disable=W0232 """An interface for accessing the vim's vars, buffer, cursors, etc.""" @staticmethod def init(): @@ -754,6 +576,80 @@ class VimInfo(with_metaclass(VimInfoMeta, object)): VimInfo.cursors.update_text_lines() +########################### About connection to server ######################### +class JSONPackageError(Exception): + """Error raised by JSONPackage.""" + pass + +class JSONPackage(object): + """Send/receive json object by gived function. + + Attributes: + content: Content of the package body. + + Static attributes: + _ENCODING: Encoding of the package. + _HEADER_LENGTH: Length of the header. + """ + _ENCODING = 'utf-8' + _HEADER_LENGTH = 10 + def __init__(self, content=None, recv_func=None): + """Constructor. + + If the receive_func is not None, it will grap the default content by + calling that function instead of by the argument "content". + + The detail of arguments/return values format see the method "recv_from". + + Args: + content: The default content of this package. + recv_func: A function for receive the default content. + """ + self.content = content + if recv_func is not None: + self.recv(recv_func) + + def send(self, send_func): + """Sends by calling the gived sending function. + + Args: + send_func: A function which will send the whole data gived. + Function format: + send_func(bytes_data): None + """ + try: + body = json.dumps(self.content) + header_str = ('%%0%dd' % JSONPackage._HEADER_LENGTH) % len(body) + send_func(header_str + body) + except TypeError as e: + raise JSONPackageError('json: %s' % str(e)) + except UnicodeError as e: + raise JSONPackageError('Cannot encode the string: %s.' % str(e)) + + def recv(self, recv_func): + """Receives a json object from a gived function. + + It will calls the give function like this: + recv_func(<num_of_bytes>) => bytes with length <num_of_bytes> + + Args: + recv_func: A function to be called to get the serialize data. + """ + try: + header_str = unicode(recv_func(JSONPackage._HEADER_LENGTH), + JSONPackage._ENCODING) + body_str = unicode(recv_func(int(header_str)), + JSONPackage._ENCODING) + except UnicodeError as e: + raise JSONPackageError('Cannot decode the bytes: %r.' % e) + except ValueError as e: + raise JSONPackageError('Cannot get the body length %r' % e) + try: + self.content = json.loads(body_str) + except ValueError as e: + raise JSONPackageError('Cannot loads to the json object: %r' % e) + + class TCPConnection(object): """My custom tcp connection. @@ -767,10 +663,9 @@ class TCPConnection(object): conn: TCP-connection. """ self._conn = conn - self._conn.settimeout( - VimInfo.bvar.get(VARNAMES.TIMEOUT, DEFAULT_NUM_GROUPS)) + self._conn.settimeout(py_bvars.get(VARNAMES.TIMEOUT, DEFAULT_TIMEOUT)) - def send(self, data): + def send_all(self, data): """Sends the data until timeout or the socket closed. Args: @@ -778,7 +673,7 @@ class TCPConnection(object): """ self._conn.sendall(data) - def recv(self, nbyte): + def recv_all(self, nbyte): """Receives the data until timeout or the socket closed. Args: @@ -809,19 +704,30 @@ class TCPClient(object): """TCP client. Attributes: - _sock: Connection. + _conn: Connection. + + Static attributes: + _conns: A dict stores connections. """ - def __init__(self): - """Constructor, automatically connects to the server.""" - try: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.connect((VimInfo.bvar[VARNAMES.SERVER_NAME], - VimInfo.bvar[VARNAMES.SERVER_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) + _conns = {} + def __init__(self, server_name, port_name): + """Constructor, automatically connects to the server. + + Args: + server_name: Server name. + port_name: Port name. + """ + key = (server_name, port_name) + if key not in TCPClient._conns: + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((server_name, port_name)) + TCPClient._conns[key] = TCPConnection(sock) + except TypeError as e: + raise TCPClientError('Cannot connect to server: %s' % str(e)) + except socket.error as e: + raise TCPClientError('Cannot connect to server: %s' % str(e)) + self._conn = TCPClient._conns[key] def request(self, req): """Sends a request to server and get the response. @@ -832,33 +738,42 @@ class TCPClient(object): Return: The response. """ - pkg = JSONPackage() - pkg.content = req - pkg.send_to(self._sock) - pkg.recv_from(self._sock) - return pkg.content + try: + JSONPackage(req).send(self._conn.send_all) + return JSONPackage(recv_func=self._conn.recv_all).content + except socket.error as e: + raise TCPClientError(e) + except JSONPackageError as e: + raise TCPClientError(e) def close(self): """Closes the socket.""" - self._sock.close() + self._conn.close() + for key, conn in TCPClient._conns.items(): + if conn is self._conn: + del TCPClient._conns[key] + break +################################ Some operations ############################### +def set_scopes(): + py_bvars.curr_scope = vim.current.buffer.number + py_wvars.curr_scope = vim.current.window.number + def get_my_info(init): """Gets my information for server. Return: The information for server. """ - return { - JSON_TOKEN.IDENTITY : VimInfo.bvar[VARNAMES.IDENTITY], - JSON_TOKEN.INIT : init, - JSON_TOKEN.MODE : VimInfo.mode, - JSON_TOKEN.CURSORS : { - CURSOR_MARK.CURRENT : VimInfo.cursors[CURSOR_MARK.CURRENT], - CURSOR_MARK.V : VimInfo.cursors[CURSOR_MARK.V], - }, - JSON_TOKEN.TEXT : VimInfo.text, - } + return {JSON_TOKEN.IDENTITY : py_bvars[VARNAMES.IDENTITY], + JSON_TOKEN.INIT : init, + JSON_TOKEN.MODE : VimInfo.mode, + JSON_TOKEN.CURSORS : { + CURSOR_MARK.CURRENT : VimInfo.cursors[CURSOR_MARK.CURRENT], + CURSOR_MARK.V : VimInfo.cursors[CURSOR_MARK.V], + }, + JSON_TOKEN.TEXT : VimInfo.text} def set_my_info(json_info): @@ -926,6 +841,7 @@ def set_other_visual(name, mode, beg, end): VimInfo.highlight[name].add_visual((row, col)) +############################## Supported operations ############################ def connect(server_name, server_port, identity): """Connects to the server. @@ -934,9 +850,10 @@ def connect(server_name, server_port, identity): server_port: Server port. identity: Identity string of this user. """ - VimInfo.bvar[VARNAMES.SERVER_NAME] = server_name - VimInfo.bvar[VARNAMES.SERVER_PORT] = int(server_port) - VimInfo.bvar[VARNAMES.IDENTITY] = identity + set_scopes() + py_bvars[VARNAMES.SERVER_NAME] = server_name + py_bvars[VARNAMES.SERVER_PORT] = int(server_port) + py_bvars[VARNAMES.IDENTITY] = identity sync(init=True) @@ -947,32 +864,48 @@ def sync(init=False): init: Flag for whether it should tell the server to reset this user or not. """ - if VARNAMES.SERVER_NAME in VimInfo.bvar: - conn = TCPClient() - response = conn.request(get_my_info(init)) - conn.close() + set_scopes() + if VARNAMES.SERVER_NAME in py_bvars: + try: + conn = TCPClient(py_bvars[VARNAMES.SERVER_NAME], + py_bvars[VARNAMES.SERVER_PORT]) + response = conn.request(get_my_info(init)) + except TCPClientError as e: + print(str(e)) + return if JSON_TOKEN.ERROR in response: - raise Exception(response[JSON_TOKEN.ERROR]) + print(response[JSON_TOKEN.ERROR]) + return set_my_info(response) set_others_info(response) - VimInfo.bvar[VARNAMES.USERS] = ', '.join( + py_bvars[VARNAMES.USERS] = ', '.join( [user[JSON_TOKEN.NICKNAME] for user in response[JSON_TOKEN.OTHERS]]) def disconnect(): """Disconnects with the server.""" - conn = TCPClient() - conn.request({JSON_TOKEN.BYE : True, - JSON_TOKEN.IDENTITY : VimInfo.bvar[VARNAMES.IDENTITY]}) - conn.close() - del VimInfo.bvar[VARNAMES.SERVER_NAME] - del VimInfo.bvar[VARNAMES.SERVER_PORT] - del VimInfo.bvar[VARNAMES.IDENTITY] - print('bye') + set_scopes() + if VARNAMES.SERVER_NAME in py_bvars: + VimInfo.highlight.reset([]) + VimInfo.highlight.render() + try: + conn = TCPClient(py_bvars[VARNAMES.SERVER_NAME], + py_bvars[VARNAMES.SERVER_PORT]) + conn.request({ + JSON_TOKEN.BYE : True, + JSON_TOKEN.IDENTITY : py_bvars[VARNAMES.IDENTITY]}) + except TCPClientError as e: + print(str(e)) + conn.close() + del py_bvars[VARNAMES.SERVER_NAME] + del py_bvars[VARNAMES.SERVER_PORT] + del py_bvars[VARNAMES.IDENTITY] + print('bye') def show_info(): """Shows the informations.""" + set_scopes() print('Highlight information:') print('Groups of normal cursor position:') for index in range(VimInfo.highlight.num_of_groups()): @@ -983,7 +916,19 @@ def show_info(): print('Groups of selection area:') for index in range(VimInfo.highlight.num_of_groups()): vim.command('hi %s' % VISUAL_GROUP(index)) - print('Users: %r' % VimInfo.bvar[VARNAMES.USERS]) + print('Users: %r' % py_bvars[VARNAMES.USERS]) + + +def set_bvar(variable_name, value): + """Sets the variable of the current buffer. + + Args: + variable_name: Variable name. + value: Value. + """ + set_scopes() + py_bvars[getattr(VARNAMES, variable_name)] = value + print('bvars[%s] = %s' % (getattr(VARNAMES, variable_name), value)) ################################## Initialize ################################## |