1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
|
"""RequestHandler."""
import bisect
import difflib
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
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.
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.
Args:
users_text_manager: An instance of UsersTextManager.
"""
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.
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.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=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, 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.
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.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.
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.DIFF] = []
for mark in request.get(JSON_TOKEN.CURSORS, []):
request[JSON_TOKEN.CURSORS][mark] = (0, 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:
request[JSON_TOKEN.DIFF] = []
|