diff options
author | cathook <b01902109@csie.ntu.edu.tw> | 2014-11-19 11:27:36 +0800 |
---|---|---|
committer | cathook <b01902109@csie.ntu.edu.tw> | 2014-11-19 11:27:36 +0800 |
commit | 9ab365e6b680abe7af6079b6030c8cc718a0a062 (patch) | |
tree | d99e5daa8bb838a59d23d42aefa865f96e192eb3 | |
parent | 96378f825fc71d917d49b3577d523cfa6167393a (diff) | |
download | vim-shrvim-9ab365e6b680abe7af6079b6030c8cc718a0a062.tar vim-shrvim-9ab365e6b680abe7af6079b6030c8cc718a0a062.tar.gz vim-shrvim-9ab365e6b680abe7af6079b6030c8cc718a0a062.tar.bz2 vim-shrvim-9ab365e6b680abe7af6079b6030c8cc718a0a062.tar.lz vim-shrvim-9ab365e6b680abe7af6079b6030c8cc718a0a062.tar.xz vim-shrvim-9ab365e6b680abe7af6079b6030c8cc718a0a062.tar.zst vim-shrvim-9ab365e6b680abe7af6079b6030c8cc718a0a062.zip |
big change...
-rw-r--r-- | server/src/authority_string_transformer.py | 6 | ||||
-rw-r--r-- | server/src/cmd_ui.py | 10 | ||||
-rw-r--r-- | server/src/log.py | 4 | ||||
-rw-r--r-- | server/src/request_handler.py | 205 | ||||
-rw-r--r-- | server/src/text_chain.py | 33 | ||||
-rw-r--r-- | server/src/users_text_manager.py | 24 | ||||
-rw-r--r-- | vim/plugin/shared_vim.vim | 373 |
7 files changed, 449 insertions, 206 deletions
diff --git a/server/src/authority_string_transformer.py b/server/src/authority_string_transformer.py index f99daa1..2d58e6e 100644 --- a/server/src/authority_string_transformer.py +++ b/server/src/authority_string_transformer.py @@ -3,7 +3,7 @@ from users_text_manager import AUTHORITY -_mappings = [ +_MAPPINGS = [ (AUTHORITY.READONLY, 'RO'), (AUTHORITY.READWRITE, 'RW'), ] @@ -23,7 +23,7 @@ def to_string(authority): Return: Corrosponding authority in string format. """ - for item in _mappings: + for item in _MAPPINGS: if item[0] == authority: return item[1] raise Error('Invalid number.') @@ -38,7 +38,7 @@ def to_number(string): Return: Corrosponding authority in number format. """ - for item in _mappings: + 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 b7079da..94cbf61 100644 --- a/server/src/cmd_ui.py +++ b/server/src/cmd_ui.py @@ -40,7 +40,7 @@ class CmdUI(cmd.Cmd): # pylint: disable=R0904 tcp_server: An instance of TCPServer. shared_vim_server: An instance of SharedVimServer. """ - super(CmdUI, self).__init__(INTRO) + super(CmdUI, self).__init__() self.prompt = PROMPT self._users_text_manager = users_text_manager self._tcp_server = tcp_server @@ -182,6 +182,14 @@ class CmdUI(cmd.Cmd): # pylint: disable=R0904 """Echo.""" print(text) + def do_eval(self, text): + """For debug, evaluate an peice of python code and prints the result.""" + try: + result = eval(text) # pylint: disable=W0123 + self.write('The result is %r\n' % result) + except Exception as e: # pylint: disable=W0703 + self.write('Exception occured: %r' % e) + def do_help(self, text): """Prints the help document, [usage] help""" try: diff --git a/server/src/log.py b/server/src/log.py index 93ec9bc..9873ade 100644 --- a/server/src/log.py +++ b/server/src/log.py @@ -14,7 +14,7 @@ def info(string): string: String to be printed. """ with _lock: - info.interface.write(string) + info.interface.write('info: ' + string) info.interface.flush() info.interface = sys.stdout # Interface of the info string to be printed at. @@ -27,7 +27,7 @@ def error(string): string: String to be printed. """ with _lock: - error.interface.write(string) + error.interface.write('error: ' + string) error.interface.flush() error.interface = sys.stderr # Interface of the error string to be printed at. diff --git a/server/src/request_handler.py b/server/src/request_handler.py index 9d820e4..458c554 100644 --- a/server/src/request_handler.py +++ b/server/src/request_handler.py @@ -1,5 +1,7 @@ """RequestHandler.""" +import bisect +import difflib import log from users_text_manager import AUTHORITY @@ -10,19 +12,145 @@ 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 + DIFF = 'diff' # Difference between this time and last time. 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 + + +def apply_patch(orig_lines, patch_info): + """Applies a patch. + + Args: + orig_lines: Original lines of the text. + patch_info: A list of replacing information. + + Return: + A list of text. + """ + new_lines, done_len = [], 0 + for beg, end, lines in patch_info: + new_lines += orig_lines[done_len : beg] + new_lines += lines + done_len = end + return new_lines + orig_lines[done_len : ] + + +def _squash_patch(patch_info): + """Squash list of replacing information to a smaller mount of information. + + Args: + patch_info: Information of patches. + + Return: + A list of replacing information. + """ + ret, index = [], 0 + while index < len(patch_info): + lines = patch_info[index][2] + index2 = index + 1 + while index2 < len(patch_info) and \ + patch_info[index2 - 1][1] >= patch_info[index2][0]: + lines += patch_info[index2][2] + index2 += 1 + ret.append((patch_info[index][0], patch_info[index2 - 1][1], lines)) + index = index2 + return ret + + +def gen_patch(orig_lines, new_lines): + """Creates a patch from two lines text. + + Args: + orig_lines: Original lines of the text. + new_lines: New lines of the text. + + Return: + A list of replacing information. + """ + diff_result = list(difflib.Differ().compare(orig_lines, new_lines)) + orig_index, ret = 0, [] + for line in diff_result: + if line.startswith(' '): + orig_index += 1 + elif line.startswith('+ '): + ret.append((orig_index, orig_index, [line[2 : ]])) + elif line.startswith('- '): + ret.append((orig_index, orig_index + 1, [])) + orig_index += 1 + return _squash_patch(ret) + + +class _CursorTransformer(object): + """Transformer for format of the cursor position. + + In vim, it use (row, col) to represent an cursor's position. + In UsersTextManager, it use numerical notation (byte distance between the + first byte in the text). + + Attributes: + _sum_len: Sum of each row. + """ + def __init__(self): + """Constructor.""" + self._sum_len = [0] + + def update_lines(self, lines): + """Update lines of text. + + Args: + lines: Lines of text. + """ + self._sum_len = [0] + for line in lines: + delta = len(line) + 1 # "+ 1" is for newline char + self._sum_len.append(self._sum_len[-1] + delta) + + def rcs_to_nums(self, rcs): + """Transform row-col format's cursor position to numerical type. + + Args: + lines: List of line text. + rcs: List of tuple of row-col format cursor postions + + Return: + A list of numerical cursor positions. + """ + ret = [] + for rc in rcs: + base = self._sum_len[min(rc[0], len(self._sum_len) - 1)] + ret.append(min(base + rc[1], self._sum_len[-1])) + return ret + + + def nums_to_rcs(self, nums): + """Transform numerical format's cursor position to row-col format. + + Args: + lines: List of line text. + nums: A list of numerical cursor positions. + + Return: + List of tuple of row-col format cursor postions + """ + ans, rmin = {}, 0 + for num in sorted(nums): + row = bisect.bisect(self._sum_len, num, lo=rmin) - 1 + col = (num - self._sum_len[row]) if row < len(self._sum_len) else 0 + rmin = row + 1 + ans[num] = (row, col) + return [ans[num] for num in nums] + class RequestHandler(object): """Handles all kinds of request. Attributes: _users_text_manager: An instance of UsersTextManager. + _cursor_transformer: An instance of _CursorTransformer. """ def __init__(self, users_text_manager): """Constructor. @@ -32,6 +160,7 @@ class RequestHandler(object): """ super(RequestHandler, self).__init__() self._users_text_manager = users_text_manager + self._cursor_transformer = _CursorTransformer() def handle(self, request): """Handles the request and returns the response. @@ -74,41 +203,68 @@ class RequestHandler(object): 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]): + if all(key in request for key in [JSON_TOKEN.INIT, JSON_TOKEN.DIFF, + 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) + lines = apply_patch( + self._users_text_manager.get_user_text(identity).split('\n'), + request[JSON_TOKEN.DIFF]) + self._cursor_transformer.update_lines(lines) + cursors = dict(zip(request[JSON_TOKEN.CURSORS].keys(), + self._cursor_transformer.rcs_to_nums( + request[JSON_TOKEN.CURSORS].values()))) 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) + UserInfo(mode=request[JSON_TOKEN.MODE], cursors=cursors), + '\n'.join(lines)) + return self._pack_sync_response( + identity, new_user_info, new_text.split('\n'), lines) - def _pack_sync_response(self, identity, user_info, text): + def _pack_sync_response(self, identity, user_info, lines, old_lines): """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. + lines: New lines of text. + old_lines: Old lines of text. + + Return: + The response json object. + """ + self._cursor_transformer.update_lines(lines) + return { + JSON_TOKEN.DIFF : gen_patch(old_lines, lines), + JSON_TOKEN.CURSORS : dict(zip( + user_info.cursors.keys(), + self._cursor_transformer.nums_to_rcs( + user_info.cursors.values()))), + JSON_TOKEN.MODE : user_info.mode, + JSON_TOKEN.OTHERS : self._pack_sync_others_response(identity) + } + + def _pack_sync_others_response(self, identity): + """Packs the response information for other users. + + Args: + identity: Identity of that user. 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() - ]} + return [ + { + JSON_TOKEN.NICKNAME : other.nick_name, + JSON_TOKEN.MODE : other.mode, + JSON_TOKEN.CURSORS: dict(zip( + other.cursors.keys(), + self._cursor_transformer.nums_to_rcs( + other.cursors.values()))) + } 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. @@ -122,9 +278,9 @@ class RequestHandler(object): if request[JSON_TOKEN.INIT]: log.info('Init the user %r\n' % identity) self._users_text_manager.reset_user(identity) - request[JSON_TOKEN.TEXT] = '' + request[JSON_TOKEN.DIFF] = [] for mark in request.get(JSON_TOKEN.CURSORS, []): - request[JSON_TOKEN.CURSORS][mark] = 0 + request[JSON_TOKEN.CURSORS][mark] = (0, 0) def _check_authority(self, identity, request): """Checks the authroity and updates the request. @@ -138,5 +294,4 @@ class RequestHandler(object): """ 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 + request[JSON_TOKEN.DIFF] = [] diff --git a/server/src/text_chain.py b/server/src/text_chain.py index d60ea02..df2c9ba 100644 --- a/server/src/text_chain.py +++ b/server/src/text_chain.py @@ -53,8 +53,10 @@ class TextChain(object): for info in cursors_info: 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._commits += [(new_id, commit), + (new_id + 1, _TextCommit(commit.text, commit.text))] self.delete(orig_id) + self.delete(self._commits[-3][0]) self._save() return new_id, commit.text, new_cursors @@ -190,7 +192,7 @@ class _TextCommit(object): @property def increased_length(self): """Gets the increased length of this commit.""" - return sum([o.increased_length for o in self._opers]) + return sum(o.increased_length for o in self._opers) def copy(self): """Returns a copy of myself. @@ -218,17 +220,9 @@ class _TextCommit(object): 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. + If the cursor position is on character which is inserted at this commit, + it will return _CursorInfo_OnNewCommit; Otherwise it will return + _CursorInfo_OnOrigText. Args: cursor_pos: Position of the cursor. @@ -236,9 +230,12 @@ class _TextCommit(object): Return: A instance of _CursorInfo_OnNewCommit or _CursorInfo_OnOrigText. """ + offset = 0 for oper in self._opers: - if oper.begin <= cursor_pos <= oper.end: - return _CursorInfo_OnNewCommit(oper, cursor_pos - oper.begin) + delta = cursor_pos - (oper.begin + offset) + if delta <= len(oper.new_text): + return _CursorInfo_OnNewCommit(oper, delta) + offset += oper.increased_length return _CursorInfo_OnOrigText(cursor_pos) def _rebase_text(self, new_orig_text): @@ -293,10 +290,12 @@ class _CursorInfo_OnNewCommit(object): def position(self): """Calculates and returns the final cursor position.""" dt = self._delta + offset = 0 for oper in self._opers: - if oper.begin + dt <= oper.end: - return oper.begin + dt + if dt <= len(oper.new_text): + return oper.begin + offset + dt dt -= len(oper.new_text) + offset += oper.increased_length class _CursorInfo_OnOrigText(object): diff --git a/server/src/users_text_manager.py b/server/src/users_text_manager.py index 93d9d94..cb86e9d 100644 --- a/server/src/users_text_manager.py +++ b/server/src/users_text_manager.py @@ -113,9 +113,9 @@ class UsersTextManager(object): 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)]) + 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. @@ -134,14 +134,14 @@ class UsersTextManager(object): 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 = _lists_to_dict(curmarks, new_curs) + self._users[identity].cursors = dict(zip(curmarks, new_curs)) for iden, user in self._users.items(): if iden == identity: continue 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) + user.cursors = dict(zip(curmarks, new_curs)) return (self._users[identity], new_text) def get_user_text(self, identity): @@ -156,17 +156,3 @@ class UsersTextManager(object): with self._rlock: 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 27e825c..d1fdf9d 100644 --- a/vim/plugin/shared_vim.vim +++ b/vim/plugin/shared_vim.vim @@ -70,11 +70,13 @@ function! _SharedVimCallPythonFunc(func_name, args) endif 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 + let level = g:shared_vim_auto_sync_level + if exists('b:shared_vim_auto_sync_level') + let level = b:shared_vim_auto_sync_level endif - if a:level <= b:shared_vim_auto_sync_level + if a:level <= level SharedVimSync endif endfunction @@ -84,9 +86,7 @@ function! _SharedVimSetup() SharedVimPython << EOF # python << EOF # ^^ Force vim highlighting the python code below. -import bisect import json -import re import socket import sys import vim @@ -116,9 +116,10 @@ class MODE: # pylint:disable=W0232 class VARNAMES: # pylint: disable=W0232 """Enumeration types of variable name in vim.""" - GROUP_NAME_PREFIX = 'gnp' # Group name's prefix. + GROUP_NAME_PREFIX = 'gnp_' # Group name's prefix. IDENTITY = 'identity' # Identity of the user. INIT = 'init' # Initial or not. + LINES = 'lines' # Lines. NUM_GROUPS = 'num_groups' # Number of groups. SERVER_NAME = 'server_name' # Server name. SERVER_PORT = 'port' # Server port. @@ -141,15 +142,15 @@ DEFAULT_NUM_GROUPS = 5 class JSON_TOKEN: # pylint:disable=W0232 """Enumeration the Ttken strings for json object.""" - BYE = 'bye' # Disconnects with the server. + BYE = 'bye' # Resets the user and do nothong. CURSORS = 'cursors' # other users' cursor position + DIFF = 'diff' # Difference between this time and last time. 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 ############### Handler for Variable stores only in python ##################### @@ -237,11 +238,7 @@ py_wvars = ScopeVars() ############################### Interface to vim ############################### class VimCursorsInfo(object): # pylint: disable=W0232 """Gets/sets the cursor position in vim.""" - def __init__(self, *_): - """Constructor.""" - self._text_num_sum = [] - - def __getitem__(self, mark): + def __getitem__(self, mark): # pylint: disable=R0201 """Gets the cursor position with specifying the mark. Args: @@ -251,86 +248,50 @@ class VimCursorsInfo(object): # pylint: disable=W0232 Cursor position. """ pos = [int(x) for x in vim.eval('getpos("%s")' % mark)] - return self.rc_to_num((pos[1] - 1, pos[2] - 1)) + row = pos[1] - 1 + line = VimInfo.transform_to_vim(VimInfo.lines[row]) + col = len(VimInfo.transform_to_py(line[ : pos[2] - 1])) + return (row, col) - def __setitem__(self, mark, value): + def __setitem__(self, mark, rc): # pylint: disable=R0201 """Sets the cursor position with specifying the mark. Args: mark: Mark of the cursor to set. + rc: The new cursor position. """ - row, col = self.num_to_rc(value) + row, col = self.transform_to_vim(rc) mark = mark if mark != CURSOR_MARK.V else CURSOR_MARK.CURRENT vim.eval('setpos("%s", [0, %d, %d, 0])' % (mark, row + 1, col + 1)) - def rc_to_num(self, rc): - """Transforms row-column cursor position to bytes position. - - Args: - rc: Row-column cursor position. - - Return: - byte position. - """ - base = self._text_num_sum[rc[0] - 1] if rc[0] > 0 else 0 - line = VimInfo.transform_to_vim(VimInfo.lines[rc[0]]) - return base + len(VimInfo.transform_to_py(line[ : rc[1]])) - - def num_to_rc(self, num, rmin=0): - """Transforms byte position to row-column cursor position. + def transform_to_vim(self, rc): # pylint: disable=R0201 + """Transform rc in utf-8 to rc in bytes. Args: - num: byte cursor position. + rc: 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]) - line = VimInfo.lines[row][ : col] if row < len(VimInfo.lines) else '' - return (row, len(VimInfo.transform_to_vim(line))) - - 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 update_text_lines(self): - """Updates the content lines. - - Args: - lines: List of content lines. + Cursor position. """ - self._text_num_sum, pre = [], 0 - for line in VimInfo.lines: - self._text_num_sum += [pre + len(line) + 1] - pre += len(line) + 1 + row = rc[0] + line = VimInfo.lines[row] if row < len(VimInfo.lines) else '' + col = len(VimInfo.transform_to_vim(line[ : rc[1]])) + return (row, col) 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. + _normal_cursor_ranges: List of highlight block ranges in normal mode. + _insert_cursor_ranges: List of highlight block ranges in insert mode. + _visual_ranges: List of highlight block ranges in visual blocks. """ def __init__(self): """Constructor.""" - self._normal_cursor_positions = [] - self._insert_cursor_positions = [] - self._visual_positions = [] + self._normal_cursor_ranges = [] + self._insert_cursor_ranges = [] + self._visual_ranges = [] def set_mode_cursor(self, mode, rc): """Sets the mode and the cursor. @@ -340,32 +301,32 @@ class GroupInfo(object): rc: The cursor position. """ if mode in (MODE.INSERT, MODE.REPLACE): - self._insert_cursor_positions += [rc] + self._insert_cursor_ranges.append((rc[0], rc[1], rc[1] + 1)) else: - self._normal_cursor_positions += [rc] + self._normal_cursor_ranges.append((rc[0], rc[1], rc[1] + 1)) - def add_visual(self, rc): + def add_visual(self, rang): """Add a visual position. Args: - rc: The position. + rang: The position. """ - self._visual_positions += [rc] + self._visual_ranges += [rang] @property - def normal_cursor_positions(self): - """Gets the normal cursor positions.""" - return self._normal_cursor_positions + def normal_cursor_ranges(self): + """Gets the normal cursor ranges.""" + return self._normal_cursor_ranges @property - def insert_cursor_positions(self): - """Gets the insert cursor positions.""" - return self._insert_cursor_positions + def insert_cursor_ranges(self): + """Gets the insert cursor ranges.""" + return self._insert_cursor_ranges @property - def visual_positions(self): - """Gets the visual positions.""" - return self._visual_positions + def visual_ranges(self): + """Gets the visual ranges.""" + return self._visual_ranges class VimHighlightInfo(object): @@ -406,11 +367,11 @@ class VimHighlightInfo(object): """Render the highlight to vim.""" for index in range(self.num_of_groups()): VimInfo.match(NORMAL_CURSOR_GROUP(index), MATCH_PRI.NORMAL, - self._groups[index].normal_cursor_positions) + self._groups[index].normal_cursor_ranges) VimInfo.match(INSERT_CURSOR_GROUP(index), MATCH_PRI.INSERT, - self._groups[index].insert_cursor_positions) + self._groups[index].insert_cursor_ranges) VimInfo.match(VISUAL_GROUP(index), MATCH_PRI.VISUAL, - self._groups[index].visual_positions) + self._groups[index].visual_ranges) def _get_group_id(self, string): """Transform the gived string to a valid group index. @@ -432,6 +393,91 @@ class VimHighlightInfo(object): return py_bvars.get(VARNAMES.NUM_GROUPS, DEFAULT_NUM_GROUPS) +class VimLinesInfo(object): + """An interface for accessing the vim's buffer. + + Attributes: + _lines: An list of lines, which is encoded text got from vim.buffer. + """ + def __init__(self): + """Constructor.""" + self._lines = [VimInfo.transform_to_py(line) + for line in vim.current.buffer] + + def __getitem__(self, index): + """Get a specified line. + + Args: + index: The line number. + """ + return self._lines[index] + + def __setitem__(self, index, text): + """Sets a specified line. + + Args: + index: The line number. + text: The new text. + """ + self._lines[index] = text + if isinstance(index, slice): + lines = [VimInfo.transform_to_vim(line) for line in text] + if index.start is not None and index.stop is not None: + vim.current.buffer[index.start : index.stop] = lines + elif index.start is None and index.stop is not None: + vim.current.buffer[ : index.stop] = lines + elif index.start is not None and index.stop is None: + vim.current.buffer[index.start : ] = lines + else: + vim.current.buffer[:] = lines + else: + vim.current.buffer[index] = VimInfo.transform_to_vim(text) + + def __len__(self): + """Gets the number of rows.""" + return len(self._lines) + + def gen_patch(self, orig_lines): + """Creates a patch from an old one. + + Args: + orig_lines: Original lines of the text. + + Return: + A list of replacing information. + """ + orig_rows, new_rows = len(orig_lines), len(self._lines) + for first in range(min(orig_rows, new_rows)): + if orig_lines[first] != self._lines[first]: + break + else: + if orig_rows < new_rows: + return [(orig_rows, orig_rows, self._lines[orig_rows : ])] + elif orig_rows > new_rows: + return [(first + 1, orig_rows, [])] + else: + return [] + delta = new_rows - orig_rows + for last in range(orig_rows - 1, first - 1, -1): + if orig_lines[last] != self._lines[last + delta]: + break + return [(first, last + 1, self._lines[first : last + delta + 1])] + + def apply_patch(self, patch_info): + """Applies a patch. + + Args: + patch_info: A list of replacing information. + + Return: + A list of text. + """ + offset = 0 + for beg, end, lines in patch_info: + self.__setitem__(slice(beg + offset, end + offset), lines) + offset += len(lines) - (end - beg) + + class VimInfoMeta(type): """An interface for accessing the vim's vars, buffer, cursors, etc. @@ -449,29 +495,16 @@ class VimInfoMeta(type): def __init__(self, *args): """Constructor.""" self._mode = None + self._lines = None - @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[:]] - - @lines.setter - def lines(self, lines): # pylint: disable=R0201 - """Sets the buffer by list of lines.""" - tr = [VimInfo.transform_to_vim(line) for line in lines] - vim.current.buffer[0 : len(vim.current.buffer)] = tr + def init(self): + """Initialize informations for this time.""" + self._lines = VimLinesInfo() @property - def text(self): - """Gets the buffer text.""" - return '\n'.join(self.lines) - - @text.setter - def text(self, text): - """Sets the buffer text.""" - lines = re.split('\n', text) - self.lines = lines if lines else [''] - VimInfo.cursors.update_text_lines() + def lines(self): + """Gets list of lines in the buffer.""" + return self._lines @property def mode(self): @@ -523,8 +556,13 @@ class VimInfoMeta(type): 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]) + rcs = [] + for row, col_beg, col_end in positions: + col_beg = VimInfo.cursors.transform_to_vim((row, col_beg))[1] + col_end = VimInfo.cursors.transform_to_vim((row, col_end))[1] + rcs += [(row, col) for col in range(col_beg, col_end)] + patterns = '\\|'.join(['\\%%%dl\\%%%dc' % (rc[0] + 1, rc[1] + 1) + for rc in rcs]) mid = int(vim.eval("matchadd('%s', '%s', %d)" % (group_name, patterns, priority))) if mid != -1: @@ -558,6 +596,37 @@ class VimInfoMeta(type): return (data if not isinstance(data, unicode) else data.encode(VimInfo.ENCODING)) + @staticmethod + def display_width(data): + """Gets the width for the data to be display. + + Args: + data: The data. + + Return: + A number. + """ + return vim.strwidth(VimInfo.transform_to_vim(data)) + + @staticmethod + def confirm(prompt, options=None, default=None): + """Confirm something from the user. + + Args: + prompt: Prompt string. + options: List of options. + default: Default option. + """ + if options is None: + ret = vim.eval('confirm("%s")' % prompt) + elif default is None: + ret = vim.eval('confirm("%s", "%s")' % + (prompt, '\\n'.join('&' + o for o in options))) + else: + ret = vim.eval('confirm("%s", "%s", "%s")' % + (prompt, '\\n'.join('&' + o for o in options), + default)) + return int(ret) - 1 # Copy from https://bitbucket.org/gutworth/six/src/c17477e81e482d34bf3cda043b2eca643084e5fd/six.py def with_metaclass(meta, *bases): @@ -570,10 +639,7 @@ def with_metaclass(meta, *bases): class VimInfo(with_metaclass(VimInfoMeta, object)): # pylint: disable=W0232 """An interface for accessing the vim's vars, buffer, cursors, etc.""" - @staticmethod - def init(): - """Initializes some settings.""" - VimInfo.cursors.update_text_lines() + pass ########################### About connection to server ######################### @@ -742,6 +808,7 @@ class TCPClient(object): JSONPackage(req).send(self._conn.send_all) return JSONPackage(recv_func=self._conn.recv_all).content except socket.error as e: + self.close() raise TCPClientError(e) except JSONPackageError as e: raise TCPClientError(e) @@ -756,9 +823,10 @@ class TCPClient(object): ################################ Some operations ############################### -def set_scopes(): - py_bvars.curr_scope = vim.current.buffer.number - py_wvars.curr_scope = vim.current.window.number +def init_for_this_time(): + py_bvars.curr_scope = vim.current.buffer + py_wvars.curr_scope = vim.current.window + VimInfo.init() def get_my_info(init): """Gets my information for server. @@ -773,7 +841,8 @@ def get_my_info(init): CURSOR_MARK.CURRENT : VimInfo.cursors[CURSOR_MARK.CURRENT], CURSOR_MARK.V : VimInfo.cursors[CURSOR_MARK.V], }, - JSON_TOKEN.TEXT : VimInfo.text} + JSON_TOKEN.DIFF : VimInfo.lines.gen_patch( + py_bvars.get(VARNAMES.LINES, ['']))} def set_my_info(json_info): @@ -782,7 +851,8 @@ def set_my_info(json_info): Args: json_info: JSON information gived by server. """ - VimInfo.text = json_info[JSON_TOKEN.TEXT] + VimInfo.lines.apply_patch(json_info[JSON_TOKEN.DIFF]) + py_bvars[VARNAMES.LINES] = VimInfo.lines[:] mode = json_info[JSON_TOKEN.MODE] VimInfo.mode = mode if mode in (MODE.VISUAL, MODE.BLOCK_VISUAL, MODE.LINE_VISUAL): @@ -805,10 +875,10 @@ def set_others_info(json_info): for user in users: name, mode = user[JSON_TOKEN.NICKNAME], user[JSON_TOKEN.MODE] cursors = user[JSON_TOKEN.CURSORS] - curr_rc = VimInfo.cursors.num_to_rc(cursors[CURSOR_MARK.CURRENT]) + curr_rc = cursors[CURSOR_MARK.CURRENT] VimInfo.highlight[name].set_mode_cursor(mode, curr_rc) if mode in (MODE.VISUAL, MODE.LINE_VISUAL, MODE.BLOCK_VISUAL): - last_rc = VimInfo.cursors.num_to_rc(cursors[CURSOR_MARK.V]) + last_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 @@ -826,19 +896,41 @@ def set_other_visual(name, mode, beg, end): """ if mode == MODE.VISUAL: for row in range(beg[0], end[0] + 1): - first = 0 if row != beg[0] else beg[1] - last = len(VimInfo.lines[row]) if row != end[0] else end[1] - for col in range(first, last + 1): - VimInfo.highlight[name].add_visual((row, col)) + col_beg = 0 if row != beg[0] else beg[1] + col_end = (len(VimInfo.lines[row]) if row != end[0] else end[1]) + 1 + VimInfo.highlight[name].add_visual((row, col_beg, col_end)) elif mode == MODE.LINE_VISUAL: for row in range(beg[0], end[0] + 1): - for col in range(0, len(VimInfo.lines[row])): - VimInfo.highlight[name].add_visual((row, col)) + VimInfo.highlight[name].add_visual( + (row, 0, len(VimInfo.lines[row]) + 1)) 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): - VimInfo.highlight[name].add_visual((row, col)) + set_other_block_visual(name, beg, end) + + +def set_other_block_visual(name, beg, end): + """Sets the other user's virtical-visual block. + + Args: + name: Name of this user. + beg: The first row-column position of the range. + end: The last row-column position of the range. + """ + w1 = VimInfo.display_width(VimInfo.lines[beg[0]][ : beg[1]]) + w2 = VimInfo.display_width(VimInfo.lines[end[0]][ : end[1]]) + left, right = min(w1, w2), max(w1, w2) + for row in range(beg[0], end[0] + 1): + for beg_col in range(len(VimInfo.lines[row]) + 1): + w = VimInfo.display_width(VimInfo.lines[row][ : beg_col]) + if left < w: + beg_col -= 1 + break + else: + continue + for end_col in range(beg_col, len(VimInfo.lines[row]) + 1): + w = VimInfo.display_width(VimInfo.lines[row][ : end_col]) + if right < w: + break + VimInfo.highlight[name].add_visual((row, beg_col, end_col)) ############################## Supported operations ############################ @@ -850,7 +942,14 @@ def connect(server_name, server_port, identity): server_port: Server port. identity: Identity string of this user. """ - set_scopes() + init_for_this_time() + if len(VimInfo.lines) > 1 or len(VimInfo.lines[0]) > 0: + c = VimInfo.confirm( + 'This tool will rewrite the whole buffer, continue? ', + ['Yes', 'No'], 'No') + if c != 0: + return + VimInfo.lines[:] = [''] py_bvars[VARNAMES.SERVER_NAME] = server_name py_bvars[VARNAMES.SERVER_PORT] = int(server_port) py_bvars[VARNAMES.IDENTITY] = identity @@ -864,7 +963,7 @@ def sync(init=False): init: Flag for whether it should tell the server to reset this user or not. """ - set_scopes() + init_for_this_time() if VARNAMES.SERVER_NAME in py_bvars: try: conn = TCPClient(py_bvars[VARNAMES.SERVER_NAME], @@ -884,7 +983,7 @@ def sync(init=False): def disconnect(): """Disconnects with the server.""" - set_scopes() + init_for_this_time() if VARNAMES.SERVER_NAME in py_bvars: VimInfo.highlight.reset([]) VimInfo.highlight.render() @@ -905,7 +1004,7 @@ def disconnect(): def show_info(): """Shows the informations.""" - set_scopes() + init_for_this_time() print('Highlight information:') print('Groups of normal cursor position:') for index in range(VimInfo.highlight.num_of_groups()): @@ -926,14 +1025,10 @@ def set_bvar(variable_name, value): variable_name: Variable name. value: Value. """ - set_scopes() + init_for_this_time() py_bvars[getattr(VARNAMES, variable_name)] = value print('bvars[%s] = %s' % (getattr(VARNAMES, variable_name), value)) - -################################## Initialize ################################## -VimInfo.init() - EOF endfunction |