def on_delete_buf(self, data): buf_id = int(data['id']) del self.FLOO_BUFS[buf_id] del self.FLOO_PATHS_TO_BUFS[data['path']] path = utils.get_full_path(data['path']) utils.rm(path) msg.warn('deleted %s because %s told me too.' % (path, data.get('username', 'the internet')))
def on_delete_buf(self, data): # TODO: somehow tell the user about this. maybe delete on disk too? del self.FLOO_BUFS[data['id']] path = utils.get_full_path(data['path']) if not G.DELETE_LOCAL_FILES: msg.log('Not deleting %s because delete_local_files is disabled' % path) return utils.rm(path) msg.warn('deleted %s because %s told me to.' % (path, data.get('username', 'the internet')))
def get_buf(self, buf_id, view=None): req = {'name': 'get_buf', 'id': buf_id} buf = self.bufs[buf_id] msg.warn('Syncing buffer %s for consistency.' % buf['path']) if 'buf' in buf: del buf['buf'] if view: view.set_read_only(True) view.set_status('Floobits', 'Floobits locked this file until it is synced.') G.AGENT.send(req)
def get_buf(buf_id, view=None): req = { 'name': 'get_buf', 'id': buf_id } buf = BUFS[buf_id] msg.warn('Syncing buffer %s for consistency.' % buf['path']) if 'buf' in buf: del buf['buf'] if view: view.set_read_only(True) view.set_status('Floobits', 'Floobits locked this file until it is synced.') G.AGENT.put(req)
def users_in_workspace(self): if not G.AGENT: return msg.warn('Not connected to a workspace.') vim.command('echom "Users connected to %s"' % (G.AGENT.workspace, )) for user in G.AGENT.workspace_info['users'].values(): vim.command('echom " %s connected with %s on %s"' % (user['username'], user['client'], user['platform']))
def part_workspace(self): if not G.AGENT: return msg.warn( 'Unable to leave workspace: You are not joined to a workspace.' ) stop_everything() msg.log('You left the workspace.')
def say_something(self): if not G.AGENT: return msg.warn('Not connected to a workspace.') something = self.vim_input( 'Say something in %s: ' % (G.AGENT.workspace, ), '') if something: G.AGENT.send_msg(something)
def maybe_new_file(self): path = self.vim.current.buffer.name if path is None or path == '': msg.debug('get:buf buffer has no filename') return if not os.path.exists(path): return if not utils.is_shared(path): msg.debug('get_buf: %s is not shared' % path) return buf = G.AGENT.get_buf_by_path(path) if not buf: if not G.IGNORE: msg.warn('G.IGNORE is not set. Uploading anyway.') G.AGENT.upload(path) if G.IGNORE and not G.IGNORE.is_ignored(path, None, True): G.AGENT.upload(path)
def tick(self): if 'patch' not in G.PERMS: self.views_changed = [] elif not self.joined_workspace: msg.debug('Not connected. Discarding view change.') self.views_changed = [] else: reported = set() to_send = [] while self.views_changed: name, v, buf = self.views_changed.pop() if 'buf' not in buf: msg.debug('No data for buf ', buf['id'], ' ', buf['path'], ' yet. Skipping sending patch') continue view = View(v, buf) if view.is_loading(): msg.debug('View for buf ', buf['id'], ' is not ready. Ignoring change event.') continue if view.native_id in reported: continue reported.add((name, view.native_id)) if name == 'patch': patch = utils.FlooPatch(view.get_text(), buf) # Update the current copy of the buffer buf['buf'] = patch.current buf['md5'] = patch.md5_after self.send(patch.to_json()) continue if name == 'saved': to_send.append({'name': 'saved', 'id': buf['id']}) continue msg.warn('Discarding unknown event in views_changed:', name) for s in to_send: self.send(s) self._status_timeout += 1 if self._status_timeout > (2000 / G.TICK_TIME): self.update_status_msg()
def on_modified(self, view, agent): buf = is_view_loaded(view) if not buf: return text = get_text(view) if buf['encoding'] != 'utf8': return msg.warn('Floobits does not support patching binary files at this time') text = text.encode('utf-8') view_md5 = hashlib.md5(text).hexdigest() bid = view.buffer_id() buf['forced_patch'] = False if view_md5 == G.VIEW_TO_HASH.get(bid): self._highlights.add(bid) return G.VIEW_TO_HASH[view.buffer_id()] = view_md5 msg.debug('changed view ', buf['path'], ' buf id ', buf['id']) self.disable_follow_mode(2000) agent.views_changed.append(('patch', view, buf))
def on_modified(self, view, agent): buf = is_view_loaded(view) if not buf: return text = get_text(view) if buf['encoding'] != 'utf8': return msg.warn('Floobits does not support patching binary files at this time') text = text.encode('utf-8') view_md5 = hashlib.md5(text).hexdigest() if view_md5 == G.VIEW_TO_HASH.get(view.buffer_id()): return G.VIEW_TO_HASH[view.buffer_id()] = view_md5 msg.debug('changed view %s buf id %s' % (buf['path'], buf['id'])) self.disable_follow_mode(2000) buf['forced_patch'] = False agent.views_changed.append((view, buf))
def on_modified(self, view): buf = is_view_loaded(view) if not buf: return text = get_text(view) if buf['encoding'] != 'utf8': return msg.warn('Floobits does not support patching binary files at this time') text = text.encode('utf-8') view_md5 = hashlib.md5(text).hexdigest() if view_md5 == G.VIEW_TO_HASH.get(view.buffer_id()): return G.VIEW_TO_HASH[view.buffer_id()] = view_md5 msg.debug('changed view %s buf id %s' % (buf['path'], buf['id'])) disable_stalker_mode(2000) buf['forced_patch'] = False self.views_changed.append((view, buf))
def on_modified(self, view, agent, activated=False, *args): buf = is_view_loaded(view) if not buf: return text = get_text(view) if buf['encoding'] != 'utf8': return msg.warn( 'Floobits does not support patching binary files at this time') text = text.encode('utf-8') view_md5 = hashlib.md5(text).hexdigest() bid = view.buffer_id() buf['forced_patch'] = False if view_md5 == G.VIEW_TO_HASH.get(bid): self._highlights.add(bid) return G.VIEW_TO_HASH[view.buffer_id()] = view_md5 msg.debug('changed view ', buf['path'], ' buf id ', buf['id']) if not activated: self.disable_follow_mode(2000) agent.views_changed.append(('patch', view, buf))
def users_in_workspace(self): if not G.AGENT: return msg.warn('Not connected to a workspace.') self.vim.command('echom "Users connected to %s"' % (G.AGENT.workspace,)) for user in G.AGENT.workspace_info['users'].values(): self.vim.command('echom " %s connected with %s on %s"' % (user['username'], user['client'], user['platform']))
def say_something(self): if not G.AGENT: return msg.warn('Not connected to a workspace.') something = self.vim_input('Say something in %s: ' % (G.AGENT.workspace,), '') if something: G.AGENT.send_msg(something)
def handler(self, name, data): if name == 'patch': Listener.apply_patch(data) elif name == 'get_buf': buf_id = data['id'] buf = listener.BUFS.get(buf_id) if not buf: return msg.warn('no buf found: %s. Hopefully you didn\'t need that' % data) timeout_id = buf.get('timeout_id') if timeout_id: utils.cancel_timeout(timeout_id) if data['encoding'] == 'base64': data['buf'] = base64.b64decode(data['buf']) # forced_patch doesn't exist in data, so this is equivalent to buf['forced_patch'] = False listener.BUFS[buf_id] = data view = listener.get_view(buf_id) if view: Listener.update_view(data, view) else: listener.save_buf(data) elif name == 'create_buf': if data['encoding'] == 'base64': data['buf'] = base64.b64decode(data['buf']) listener.BUFS[data['id']] = data listener.PATHS_TO_IDS[data['path']] = data['id'] listener.save_buf(data) cb = listener.CREATE_BUF_CBS.get(data['path']) if cb: del listener.CREATE_BUF_CBS[data['path']] try: cb(data['id']) except Exception as e: print(e) elif name == 'rename_buf': del listener.PATHS_TO_IDS[data['old_path']] listener.PATHS_TO_IDS[data['path']] = data['id'] new = utils.get_full_path(data['path']) old = utils.get_full_path(data['old_path']) new_dir = os.path.split(new)[0] if new_dir: utils.mkdir(new_dir) os.rename(old, new) view = listener.get_view(data['id']) if view: view.retarget(new) listener.BUFS[data['id']]['path'] = data['path'] elif name == 'delete_buf': path = utils.get_full_path(data['path']) listener.delete_buf(data['id']) try: utils.rm(path) except Exception: pass user_id = data.get('user_id') username = self.get_username_by_id(user_id) msg.log('%s deleted %s' % (username, path)) elif name == 'room_info': Listener.reset() G.JOINED_WORKSPACE = True # Success! Reset counter self.reconnect_delay = self.INITIAL_RECONNECT_DELAY self.retries = self.MAX_RETRIES self.workspace_info = data G.PERMS = data['perms'] if 'patch' not in data['perms']: msg.log('No patch permission. Setting buffers to read-only') if sublime.ok_cancel_dialog('You don\'t have permission to edit this workspace. All files will be read-only.\n\nDo you want to request edit permission?'): self.put({'name': 'request_perms', 'perms': ['edit_room']}) project_json = { 'folders': [ {'path': G.PROJECT_PATH} ] } utils.mkdir(G.PROJECT_PATH) with open(os.path.join(G.PROJECT_PATH, '.sublime-project'), 'wb') as project_fd: project_fd.write(json.dumps(project_json, indent=4, sort_keys=True).encode('utf-8')) floo_json = { 'url': utils.to_workspace_url({ 'host': self.host, 'owner': self.owner, 'port': self.port, 'workspace': self.workspace, 'secure': self.secure, }) } with open(os.path.join(G.PROJECT_PATH, '.floo'), 'w') as floo_fd: floo_fd.write(json.dumps(floo_json, indent=4, sort_keys=True)) for buf_id, buf in data['bufs'].items(): buf_id = int(buf_id) # json keys must be strings buf_path = utils.get_full_path(buf['path']) new_dir = os.path.dirname(buf_path) utils.mkdir(new_dir) listener.BUFS[buf_id] = buf listener.PATHS_TO_IDS[buf['path']] = buf_id # TODO: stupidly inefficient view = listener.get_view(buf_id) if view and not view.is_loading() and buf['encoding'] == 'utf8': view_text = listener.get_text(view) view_md5 = hashlib.md5(view_text.encode('utf-8')).hexdigest() if view_md5 == buf['md5']: msg.debug('md5 sum matches view. not getting buffer %s' % buf['path']) buf['buf'] = view_text G.VIEW_TO_HASH[view.buffer_id()] = view_md5 elif self.get_bufs: Listener.get_buf(buf_id) #TODO: maybe send patch here? else: try: buf_fd = open(buf_path, 'rb') buf_buf = buf_fd.read() md5 = hashlib.md5(buf_buf).hexdigest() if md5 == buf['md5']: msg.debug('md5 sum matches. not getting buffer %s' % buf['path']) if buf['encoding'] == 'utf8': buf_buf = buf_buf.decode('utf-8') buf['buf'] = buf_buf elif self.get_bufs: Listener.get_buf(buf_id) except Exception as e: msg.debug('Error calculating md5:', e) Listener.get_buf(buf_id) msg.log('Successfully joined workspace %s/%s' % (self.owner, self.workspace)) temp_data = data.get('temp_data', {}) hangout = temp_data.get('hangout', {}) hangout_url = hangout.get('url') if hangout_url: self.prompt_join_hangout(hangout_url) if self.on_room_info: self.on_room_info() self.on_room_info = None elif name == 'user_info': user_id = str(data['user_id']) user_info = data['user_info'] self.workspace_info['users'][user_id] = user_info if user_id == str(self.workspace_info['user_id']): G.PERMS = user_info['perms'] elif name == 'join': msg.log('%s joined the workspace' % data['username']) user_id = str(data['user_id']) self.workspace_info['users'][user_id] = data elif name == 'part': msg.log('%s left the workspace' % data['username']) user_id = str(data['user_id']) try: del self.workspace_info['users'][user_id] except Exception as e: print('Unable to delete user %s from user list' % (data)) region_key = 'floobits-highlight-%s' % (user_id) for window in sublime.windows(): for view in window.views(): view.erase_regions(region_key) elif name == 'highlight': region_key = 'floobits-highlight-%s' % (data['user_id']) Listener.highlight(data['id'], region_key, data['username'], data['ranges'], data.get('ping', False)) elif name == 'set_temp_data': hangout_data = data.get('data', {}) hangout = hangout_data.get('hangout', {}) hangout_url = hangout.get('url') if hangout_url: self.prompt_join_hangout(hangout_url) elif name == 'saved': try: buf = listener.BUFS[data['id']] username = self.get_username_by_id(data['user_id']) msg.log('%s saved buffer %s' % (username, buf['path'])) except Exception as e: msg.error(str(e)) elif name == 'request_perms': print(data) user_id = str(data.get('user_id')) username = self.get_username_by_id(user_id) if not username: return msg.debug('Unknown user for id %s. Not handling request_perms event.' % user_id) perm_mapping = { 'edit_room': 'edit', 'admin_room': 'admin', } perms = data.get('perms') perms_str = ''.join([perm_mapping.get(p) for p in perms]) prompt = 'User %s is requesting %s permission for this room.' % (username, perms_str) message = data.get('message') if message: prompt += '\n\n%s says: %s' % (username, message) prompt += '\n\nDo you want to grant them permission?' confirm = bool(sublime.ok_cancel_dialog(prompt)) if confirm: action = 'add' else: action = 'reject' self.put({ 'name': 'perms', 'action': action, 'user_id': user_id, 'perms': perms }) elif name == 'perms': action = data['action'] user_id = str(data['user_id']) user = self.workspace_info['users'].get(user_id) if user is None: msg.log('No user for id %s. Not handling perms event' % user_id) return perms = set(user['perms']) if action == 'add': perms |= set(data['perms']) elif action == 'remove': perms -= set(data['perms']) else: return user['perms'] = list(perms) if user_id == self.workspace_info['user_id']: G.PERMS = perms elif name == 'msg': self.on_msg(data) else: msg.debug('unknown name!', name, 'data:', data)
def part_workspace(self): if not G.AGENT: return msg.warn('Unable to leave workspace: You are not joined to a workspace.') stop_everything() msg.log('You left the workspace.')
def apply_patch(patch_data): if not G.AGENT: msg.debug('Not connected. Discarding view change.') return buf_id = patch_data['id'] buf = BUFS[buf_id] if 'buf' not in buf: msg.debug('buf %s not populated yet. not patching' % buf['path']) return if buf['encoding'] == 'base64': # TODO apply binary patches return Listener.get_buf(buf_id, None) view = get_view(buf_id) if len(patch_data['patch']) == 0: msg.error('wtf? no patches to apply. server is being stupid') return msg.debug('patch is', patch_data['patch']) dmp_patches = DMP.patch_fromText(patch_data['patch']) # TODO: run this in a separate thread old_text = buf['buf'] if view and not view.is_loading(): view_text = get_text(view) if old_text == view_text: buf['forced_patch'] = False elif not buf.get('forced_patch'): patch = utils.FlooPatch(get_text(view), buf) # Update the current copy of the buffer buf['buf'] = patch.current buf['md5'] = hashlib.md5(patch.current.encode('utf-8')).hexdigest() buf['forced_patch'] = True msg.debug('forcing patch for %s' % buf['path']) G.AGENT.put(patch.to_json()) old_text = view_text else: msg.debug('forced patch is true. not sending another patch for buf %s' % buf['path']) md5_before = hashlib.md5(old_text.encode('utf-8')).hexdigest() if md5_before != patch_data['md5_before']: msg.warn('starting md5s don\'t match for %s. this is dangerous!' % buf['path']) t = DMP.patch_apply(dmp_patches, old_text) clean_patch = True for applied_patch in t[1]: if not applied_patch: clean_patch = False break if G.DEBUG: if len(t[0]) == 0: try: msg.debug('OMG EMPTY!') msg.debug('Starting data:', buf['buf']) msg.debug('Patch:', patch_data['patch']) except Exception as e: print(e) if '\x01' in t[0]: msg.debug('FOUND CRAZY BYTE IN BUFFER') msg.debug('Starting data:', buf['buf']) msg.debug('Patch:', patch_data['patch']) timeout_id = buf.get('timeout_id') if timeout_id: utils.cancel_timeout(timeout_id) if not clean_patch: msg.log('Couldn\'t patch %s cleanly.' % buf['path']) return Listener.get_buf(buf_id, view) cur_hash = hashlib.md5(t[0].encode('utf-8')).hexdigest() if cur_hash != patch_data['md5_after']: buf['timeout_id'] = utils.set_timeout(Listener.get_buf, 2000, buf_id, view) buf['buf'] = t[0] buf['md5'] = cur_hash if not view: save_buf(buf) return regions = [] commands = [] for patch in t[2]: offset = patch[0] length = patch[1] patch_text = patch[2] region = sublime.Region(offset, offset + length) regions.append(region) commands.append({'r': [offset, offset + length], 'data': patch_text}) view.run_command('floo_view_replace_regions', {'commands': commands}) region_key = 'floobits-patch-' + patch_data['username'] view.add_regions(region_key, regions, 'floobits.patch', 'circle', sublime.DRAW_OUTLINED) utils.set_timeout(view.erase_regions, 2000, region_key) view.set_status('Floobits', 'Changed by %s at %s' % (patch_data['username'], datetime.now().strftime('%H:%M')))
def handler(self, name, data): if name == "patch": Listener.apply_patch(data) elif name == "get_buf": buf_id = data["id"] buf = listener.BUFS.get(buf_id) if not buf: return msg.warn("no buf found: %s. Hopefully you didn't need that" % data) timeout_id = buf.get("timeout_id") if timeout_id: utils.cancel_timeout(timeout_id) if data["encoding"] == "base64": data["buf"] = base64.b64decode(data["buf"]) # forced_patch doesn't exist in data, so this is equivalent to buf['forced_patch'] = False listener.BUFS[buf_id] = data view = listener.get_view(buf_id) if view: Listener.update_view(data, view) else: listener.save_buf(data) elif name == "create_buf": if data["encoding"] == "base64": data["buf"] = base64.b64decode(data["buf"]) listener.BUFS[data["id"]] = data listener.PATHS_TO_IDS[data["path"]] = data["id"] listener.save_buf(data) cb = listener.CREATE_BUF_CBS.get(data["path"]) if cb: del listener.CREATE_BUF_CBS[data["path"]] try: cb(data["id"]) except Exception as e: print(e) elif name == "rename_buf": del listener.PATHS_TO_IDS[data["old_path"]] listener.PATHS_TO_IDS[data["path"]] = data["id"] new = utils.get_full_path(data["path"]) old = utils.get_full_path(data["old_path"]) new_dir = os.path.split(new)[0] if new_dir: utils.mkdir(new_dir) os.rename(old, new) view = listener.get_view(data["id"]) if view: view.retarget(new) listener.BUFS[data["id"]]["path"] = data["path"] elif name == "delete_buf": path = utils.get_full_path(data["path"]) listener.delete_buf(data["id"]) try: utils.rm(path) except Exception: pass user_id = data.get("user_id") username = self.get_username_by_id(user_id) msg.log("%s deleted %s" % (username, path)) elif name == "room_info": Listener.reset() G.JOINED_WORKSPACE = True # Success! Reset counter self.reconnect_delay = self.INITIAL_RECONNECT_DELAY self.retries = self.MAX_RETRIES self.workspace_info = data G.PERMS = data["perms"] if "patch" not in data["perms"]: msg.log("No patch permission. Setting buffers to read-only") if sublime.ok_cancel_dialog( "You don't have permission to edit this workspace. All files will be read-only.\n\nDo you want to request edit permission?" ): self.put({"name": "request_perms", "perms": ["edit_room"]}) project_json = {"folders": [{"path": G.PROJECT_PATH}]} utils.mkdir(G.PROJECT_PATH) with open(os.path.join(G.PROJECT_PATH, ".sublime-project"), "wb") as project_fd: project_fd.write(json.dumps(project_json, indent=4, sort_keys=True).encode("utf-8")) floo_json = { "url": utils.to_workspace_url( { "host": self.host, "owner": self.owner, "port": self.port, "workspace": self.workspace, "secure": self.secure, } ) } with open(os.path.join(G.PROJECT_PATH, ".floo"), "w") as floo_fd: floo_fd.write(json.dumps(floo_json, indent=4, sort_keys=True)) for buf_id, buf in data["bufs"].items(): buf_id = int(buf_id) # json keys must be strings buf_path = utils.get_full_path(buf["path"]) new_dir = os.path.dirname(buf_path) utils.mkdir(new_dir) listener.BUFS[buf_id] = buf listener.PATHS_TO_IDS[buf["path"]] = buf_id # TODO: stupidly inefficient view = listener.get_view(buf_id) if view and not view.is_loading() and buf["encoding"] == "utf8": view_text = listener.get_text(view) view_md5 = hashlib.md5(view_text.encode("utf-8")).hexdigest() if view_md5 == buf["md5"]: msg.debug("md5 sum matches view. not getting buffer %s" % buf["path"]) buf["buf"] = view_text G.VIEW_TO_HASH[view.buffer_id()] = view_md5 elif self.get_bufs: Listener.get_buf(buf_id) # TODO: maybe send patch here? else: try: buf_fd = open(buf_path, "rb") buf_buf = buf_fd.read() md5 = hashlib.md5(buf_buf).hexdigest() if md5 == buf["md5"]: msg.debug("md5 sum matches. not getting buffer %s" % buf["path"]) if buf["encoding"] == "utf8": buf_buf = buf_buf.decode("utf-8") buf["buf"] = buf_buf elif self.get_bufs: Listener.get_buf(buf_id) except Exception as e: msg.debug("Error calculating md5:", e) Listener.get_buf(buf_id) msg.log("Successfully joined workspace %s/%s" % (self.owner, self.workspace)) temp_data = data.get("temp_data", {}) hangout = temp_data.get("hangout", {}) hangout_url = hangout.get("url") if hangout_url: self.prompt_join_hangout(hangout_url) if self.on_room_info: self.on_room_info() self.on_room_info = None elif name == "user_info": user_id = str(data["user_id"]) user_info = data["user_info"] self.workspace_info["users"][user_id] = user_info if user_id == str(self.workspace_info["user_id"]): G.PERMS = user_info["perms"] elif name == "join": msg.log("%s joined the workspace" % data["username"]) user_id = str(data["user_id"]) self.workspace_info["users"][user_id] = data elif name == "part": msg.log("%s left the workspace" % data["username"]) user_id = str(data["user_id"]) try: del self.workspace_info["users"][user_id] except Exception as e: print("Unable to delete user %s from user list" % (data)) region_key = "floobits-highlight-%s" % (user_id) for window in sublime.windows(): for view in window.views(): view.erase_regions(region_key) elif name == "highlight": region_key = "floobits-highlight-%s" % (data["user_id"]) Listener.highlight(data["id"], region_key, data["username"], data["ranges"], data.get("ping", False)) elif name == "set_temp_data": hangout_data = data.get("data", {}) hangout = hangout_data.get("hangout", {}) hangout_url = hangout.get("url") if hangout_url: self.prompt_join_hangout(hangout_url) elif name == "saved": try: buf = listener.BUFS[data["id"]] username = self.get_username_by_id(data["user_id"]) msg.log("%s saved buffer %s" % (username, buf["path"])) except Exception as e: msg.error(str(e)) elif name == "request_perms": print(data) user_id = str(data.get("user_id")) username = self.get_username_by_id(user_id) if not username: return msg.debug("Unknown user for id %s. Not handling request_perms event." % user_id) perm_mapping = {"edit_room": "edit", "admin_room": "admin"} perms = data.get("perms") perms_str = "".join([perm_mapping.get(p) for p in perms]) prompt = "User %s is requesting %s permission for this room." % (username, perms_str) message = data.get("message") if message: prompt += "\n\n%s says: %s" % (username, message) prompt += "\n\nDo you want to grant them permission?" confirm = bool(sublime.ok_cancel_dialog(prompt)) if confirm: action = "add" else: action = "reject" self.put({"name": "perms", "action": action, "user_id": user_id, "perms": perms}) elif name == "perms": action = data["action"] user_id = str(data["user_id"]) user = self.workspace_info["users"].get(user_id) if user is None: msg.log("No user for id %s. Not handling perms event" % user_id) return perms = set(user["perms"]) if action == "add": perms |= set(data["perms"]) elif action == "remove": perms -= set(data["perms"]) else: return user["perms"] = list(perms) if user_id == self.workspace_info["user_id"]: G.PERMS = perms elif name == "msg": self.on_msg(data) else: msg.debug("unknown name!", name, "data:", data)
def _on_highlight(self, data, clone=True): region_key = 'floobits-highlight-%s' % (data['user_id']) buf_id = int(data['id']) username = data['username'] ranges = data['ranges'] summon = data.get('summon', False) user_id = str(data['user_id']) following = data.get('following', False) msg.debug( str([ buf_id, region_key, user_id, username, ranges, summon, following, clone ])) if not ranges: msg.warn('Ignoring empty highlight from ', username) return buf = self.bufs.get(buf_id) if not buf: return # TODO: move this state machine into one variable b = self.on_load.get(buf_id) if b and b.get('highlight'): msg.debug('ignoring command until on_load is complete') return if buf_id in self.on_clone: msg.debug('ignoring command until on_clone is complete') return if buf_id in self.temp_ignore_highlight: msg.debug( 'ignoring command until temp_ignore_highlight is complete') return if summon or not following: self.last_highlight = data self.last_highlight_by_user[username] = data do_stuff = summon if G.FOLLOW_MODE and not summon: if self.temp_disable_follow or following: do_stuff = False elif G.FOLLOW_USERS: do_stuff = username in G.FOLLOW_USERS else: do_stuff = True view = self.get_view(buf_id) if not view or view.is_loading(): if do_stuff: msg.debug('creating view') create_view(buf) self.on_load[buf_id]['highlight'] = lambda: self._on_highlight( data, False) return view = view.view regions = [] for r in ranges: # Ranges with a length of zero are invisible in Sublime if r[0] == r[1]: r[1] += 1 regions.append(sublime.Region(*r)) def swap_regions(v, following=False): v.erase_regions(region_key) if following: draw = sublime.HIDDEN else: draw = sublime.DRAW_OUTLINED v.add_regions(region_key, regions, region_key, 'dot', draw) if not do_stuff: return swap_regions(view, following) win = G.WORKSPACE_WINDOW if not G.SPLIT_MODE: win.focus_view(view) swap_regions(view) # Explicit summon by another user. Center the line. if summon: view.show_at_center(regions[0]) # Avoid scrolling/jumping lots in follow mode else: view.show(regions[0]) return focus_group = win.num_groups() - 1 view_in_group = get_view_in_group(view.buffer_id(), focus_group) if view_in_group: msg.debug('view in group') win.focus_view(view_in_group) swap_regions(view_in_group) utils.set_timeout(win.focus_group, 0, 0) return view_in_group.show(regions[0]) if not clone: msg.debug('no clone... moving ', view.buffer_id(), win.num_groups() - 1, 0) win.focus_view(view) win.set_view_index(view, win.num_groups() - 1, 0) def dont_crash_sublime(): utils.set_timeout(win.focus_group, 0, 0) swap_regions(view) return view.show(regions[0]) return utils.set_timeout(dont_crash_sublime, 0) msg.debug('View not in group... cloning') win.focus_view(view) def on_clone(buf, view): msg.debug('on clone') def poll_for_move(): msg.debug('poll_for_move') win.focus_view(view) win.set_view_index(view, win.num_groups() - 1, 0) if not get_view_in_group(view.buffer_id(), focus_group): return utils.set_timeout(poll_for_move, 20) msg.debug('found view, now moving ', view.name(), win.num_groups() - 1) swap_regions(view) view.show(regions[0]) win.focus_view(view) utils.set_timeout(win.focus_group, 0, 0) try: del self.temp_ignore_highlight[buf_id] except Exception: pass utils.set_timeout(win.focus_group, 0, 0) poll_for_move() self.on_clone[buf_id] = on_clone self.temp_ignore_highlight[buf_id] = True win.run_command('clone_file') return win.focus_group(0)
def handler(self, name, data): if name == 'patch': Listener.apply_patch(data) elif name == 'get_buf': buf_id = data['id'] buf = listener.BUFS.get(buf_id) if not buf: return msg.warn( 'no buf found: %s. Hopefully you didn\'t need that' % data) timeout_id = buf.get('timeout_id') if timeout_id: utils.cancel_timeout(timeout_id) if data['encoding'] == 'base64': data['buf'] = base64.b64decode(data['buf']) # forced_patch doesn't exist in data, so this is equivalent to buf['forced_patch'] = False listener.BUFS[buf_id] = data view = listener.get_view(buf_id) if view: Listener.update_view(data, view) else: listener.save_buf(data) elif name == 'create_buf': if data['encoding'] == 'base64': data['buf'] = base64.b64decode(data['buf']) listener.BUFS[data['id']] = data listener.PATHS_TO_IDS[data['path']] = data['id'] listener.save_buf(data) cb = listener.CREATE_BUF_CBS.get(data['path']) if cb: del listener.CREATE_BUF_CBS[data['path']] try: cb(data['id']) except Exception as e: print(e) elif name == 'rename_buf': del listener.PATHS_TO_IDS[data['old_path']] listener.PATHS_TO_IDS[data['path']] = data['id'] new = utils.get_full_path(data['path']) old = utils.get_full_path(data['old_path']) new_dir = os.path.split(new)[0] if new_dir: utils.mkdir(new_dir) os.rename(old, new) view = listener.get_view(data['id']) if view: view.retarget(new) listener.BUFS[data['id']]['path'] = data['path'] elif name == 'delete_buf': path = utils.get_full_path(data['path']) listener.delete_buf(data['id']) try: utils.rm(path) except Exception: pass user_id = data.get('user_id') username = self.get_username_by_id(user_id) msg.log('%s deleted %s' % (username, path)) elif name == 'room_info': Listener.reset() G.JOINED_WORKSPACE = True # Success! Reset counter self.reconnect_delay = self.INITIAL_RECONNECT_DELAY self.retries = self.MAX_RETRIES self.workspace_info = data G.PERMS = data['perms'] if 'patch' not in data['perms']: msg.log('No patch permission. Setting buffers to read-only') if sublime.ok_cancel_dialog( 'You don\'t have permission to edit this workspace. All files will be read-only.\n\nDo you want to request edit permission?' ): self.put({'name': 'request_perms', 'perms': ['edit_room']}) project_json = {'folders': [{'path': G.PROJECT_PATH}]} utils.mkdir(G.PROJECT_PATH) with open(os.path.join(G.PROJECT_PATH, '.sublime-project'), 'wb') as project_fd: project_fd.write( json.dumps(project_json, indent=4, sort_keys=True).encode('utf-8')) floo_json = { 'url': utils.to_workspace_url({ 'host': self.host, 'owner': self.owner, 'port': self.port, 'workspace': self.workspace, 'secure': self.secure, }) } with open(os.path.join(G.PROJECT_PATH, '.floo'), 'w') as floo_fd: floo_fd.write(json.dumps(floo_json, indent=4, sort_keys=True)) for buf_id, buf in data['bufs'].items(): buf_id = int(buf_id) # json keys must be strings buf_path = utils.get_full_path(buf['path']) new_dir = os.path.dirname(buf_path) utils.mkdir(new_dir) listener.BUFS[buf_id] = buf listener.PATHS_TO_IDS[buf['path']] = buf_id # TODO: stupidly inefficient view = listener.get_view(buf_id) if view and not view.is_loading( ) and buf['encoding'] == 'utf8': view_text = listener.get_text(view) view_md5 = hashlib.md5( view_text.encode('utf-8')).hexdigest() if view_md5 == buf['md5']: msg.debug( 'md5 sum matches view. not getting buffer %s' % buf['path']) buf['buf'] = view_text G.VIEW_TO_HASH[view.buffer_id()] = view_md5 elif self.get_bufs: Listener.get_buf(buf_id) #TODO: maybe send patch here? else: try: buf_fd = open(buf_path, 'rb') buf_buf = buf_fd.read() md5 = hashlib.md5(buf_buf).hexdigest() if md5 == buf['md5']: msg.debug( 'md5 sum matches. not getting buffer %s' % buf['path']) if buf['encoding'] == 'utf8': buf_buf = buf_buf.decode('utf-8') buf['buf'] = buf_buf elif self.get_bufs: Listener.get_buf(buf_id) except Exception as e: msg.debug('Error calculating md5:', e) Listener.get_buf(buf_id) msg.log('Successfully joined workspace %s/%s' % (self.owner, self.workspace)) temp_data = data.get('temp_data', {}) hangout = temp_data.get('hangout', {}) hangout_url = hangout.get('url') if hangout_url: self.prompt_join_hangout(hangout_url) if self.on_room_info: self.on_room_info() self.on_room_info = None elif name == 'user_info': user_id = str(data['user_id']) user_info = data['user_info'] self.workspace_info['users'][user_id] = user_info if user_id == str(self.workspace_info['user_id']): G.PERMS = user_info['perms'] elif name == 'join': msg.log('%s joined the workspace' % data['username']) user_id = str(data['user_id']) self.workspace_info['users'][user_id] = data elif name == 'part': msg.log('%s left the workspace' % data['username']) user_id = str(data['user_id']) try: del self.workspace_info['users'][user_id] except Exception as e: print('Unable to delete user %s from user list' % (data)) region_key = 'floobits-highlight-%s' % (user_id) for window in sublime.windows(): for view in window.views(): view.erase_regions(region_key) elif name == 'highlight': region_key = 'floobits-highlight-%s' % (data['user_id']) Listener.highlight(data['id'], region_key, data['username'], data['ranges'], data.get('ping', False)) elif name == 'set_temp_data': hangout_data = data.get('data', {}) hangout = hangout_data.get('hangout', {}) hangout_url = hangout.get('url') if hangout_url: self.prompt_join_hangout(hangout_url) elif name == 'saved': try: buf = listener.BUFS[data['id']] username = self.get_username_by_id(data['user_id']) msg.log('%s saved buffer %s' % (username, buf['path'])) except Exception as e: msg.error(str(e)) elif name == 'request_perms': print(data) user_id = str(data.get('user_id')) username = self.get_username_by_id(user_id) if not username: return msg.debug( 'Unknown user for id %s. Not handling request_perms event.' % user_id) perm_mapping = { 'edit_room': 'edit', 'admin_room': 'admin', } perms = data.get('perms') perms_str = ''.join([perm_mapping.get(p) for p in perms]) prompt = 'User %s is requesting %s permission for this room.' % ( username, perms_str) message = data.get('message') if message: prompt += '\n\n%s says: %s' % (username, message) prompt += '\n\nDo you want to grant them permission?' confirm = bool(sublime.ok_cancel_dialog(prompt)) if confirm: action = 'add' else: action = 'reject' self.put({ 'name': 'perms', 'action': action, 'user_id': user_id, 'perms': perms }) elif name == 'perms': action = data['action'] user_id = str(data['user_id']) user = self.workspace_info['users'].get(user_id) if user is None: msg.log('No user for id %s. Not handling perms event' % user_id) return perms = set(user['perms']) if action == 'add': perms |= set(data['perms']) elif action == 'remove': perms -= set(data['perms']) else: return user['perms'] = list(perms) if user_id == self.workspace_info['user_id']: G.PERMS = perms elif name == 'msg': self.on_msg(data) else: msg.debug('unknown name!', name, 'data:', data)
def apply_patch(patch_data): if not G.AGENT: msg.debug('Not connected. Discarding view change.') return buf_id = patch_data['id'] buf = BUFS[buf_id] if 'buf' not in buf: msg.debug('buf %s not populated yet. not patching' % buf['path']) return if buf['encoding'] == 'base64': # TODO apply binary patches return Listener.get_buf(buf_id, None) view = get_view(buf_id) if len(patch_data['patch']) == 0: msg.error('wtf? no patches to apply. server is being stupid') return msg.debug('patch is', patch_data['patch']) dmp_patches = DMP.patch_fromText(patch_data['patch']) # TODO: run this in a separate thread old_text = buf['buf'] if view and not view.is_loading(): view_text = get_text(view) if old_text == view_text: buf['forced_patch'] = False elif not buf.get('forced_patch'): patch = utils.FlooPatch(get_text(view), buf) # Update the current copy of the buffer buf['buf'] = patch.current buf['md5'] = hashlib.md5( patch.current.encode('utf-8')).hexdigest() buf['forced_patch'] = True msg.debug('forcing patch for %s' % buf['path']) G.AGENT.put(patch.to_json()) old_text = view_text else: msg.debug( 'forced patch is true. not sending another patch for buf %s' % buf['path']) md5_before = hashlib.md5(old_text.encode('utf-8')).hexdigest() if md5_before != patch_data['md5_before']: msg.warn('starting md5s don\'t match for %s. this is dangerous!' % buf['path']) t = DMP.patch_apply(dmp_patches, old_text) clean_patch = True for applied_patch in t[1]: if not applied_patch: clean_patch = False break if G.DEBUG: if len(t[0]) == 0: try: msg.debug('OMG EMPTY!') msg.debug('Starting data:', buf['buf']) msg.debug('Patch:', patch_data['patch']) except Exception as e: print(e) if '\x01' in t[0]: msg.debug('FOUND CRAZY BYTE IN BUFFER') msg.debug('Starting data:', buf['buf']) msg.debug('Patch:', patch_data['patch']) timeout_id = buf.get('timeout_id') if timeout_id: utils.cancel_timeout(timeout_id) if not clean_patch: msg.log('Couldn\'t patch %s cleanly.' % buf['path']) return Listener.get_buf(buf_id, view) cur_hash = hashlib.md5(t[0].encode('utf-8')).hexdigest() if cur_hash != patch_data['md5_after']: buf['timeout_id'] = utils.set_timeout(Listener.get_buf, 2000, buf_id, view) buf['buf'] = t[0] buf['md5'] = cur_hash if not view: save_buf(buf) return regions = [] commands = [] for patch in t[2]: offset = patch[0] length = patch[1] patch_text = patch[2] region = sublime.Region(offset, offset + length) regions.append(region) commands.append({ 'r': [offset, offset + length], 'data': patch_text }) view.run_command('floo_view_replace_regions', {'commands': commands}) region_key = 'floobits-patch-' + patch_data['username'] view.add_regions(region_key, regions, 'floobits.patch', 'circle', sublime.DRAW_OUTLINED) utils.set_timeout(view.erase_regions, 2000, region_key) view.set_status( 'Floobits', 'Changed by %s at %s' % (patch_data['username'], datetime.now().strftime('%H:%M')))
def _on_highlight(self, data, clone=True): region_key = 'floobits-highlight-%s' % (data['user_id']) buf_id = int(data['id']) username = data['username'] ranges = data['ranges'] summon = data.get('ping', False) user_id = str(data['user_id']) msg.debug(str([buf_id, region_key, user_id, username, ranges, summon, data.get('following'), clone])) if not ranges: msg.warn('Ignoring empty highlight from ', username) return buf = self.bufs.get(buf_id) if not buf: return # TODO: move this state machine into one variable b = self.on_load.get(buf_id) if b and b.get('highlight'): msg.debug('ignoring command until on_load is complete') return if buf_id in self.on_clone: msg.debug('ignoring command until on_clone is complete') return if buf_id in self.temp_ignore_highlight: msg.debug('ignoring command until temp_ignore_highlight is complete') return if summon or not data.get('following'): self.last_highlight = data self.last_highlight_by_user[username] = data do_stuff = summon if G.FOLLOW_MODE and not summon: if self.temp_disable_follow or data.get('following'): do_stuff = False elif G.FOLLOW_USERS: do_stuff = username in G.FOLLOW_USERS else: do_stuff = True view = self.get_view(buf_id) if not view or view.is_loading(): if do_stuff: msg.debug('creating view') create_view(buf) self.on_load[buf_id]['highlight'] = lambda: self._on_highlight(data, False) return view = view.view regions = [] for r in ranges: # TODO: add one to the ranges that have a length of zero regions.append(sublime.Region(*r)) def swap_regions(v): v.erase_regions(region_key) v.add_regions(region_key, regions, region_key, 'dot', sublime.DRAW_OUTLINED) if not do_stuff: return swap_regions(view) win = G.WORKSPACE_WINDOW if not G.SPLIT_MODE: win.focus_view(view) swap_regions(view) # Explicit summon by another user. Center the line. if summon: view.show_at_center(regions[0]) # Avoid scrolling/jumping lots in follow mode else: view.show(regions[0]) return focus_group = win.num_groups() - 1 view_in_group = get_view_in_group(view.buffer_id(), focus_group) if view_in_group: msg.debug('view in group') win.focus_view(view_in_group) swap_regions(view_in_group) utils.set_timeout(win.focus_group, 0, 0) return view_in_group.show(regions[0]) if not clone: msg.debug('no clone... moving ', view.buffer_id(), win.num_groups() - 1, 0) win.focus_view(view) win.set_view_index(view, win.num_groups() - 1, 0) def dont_crash_sublime(): utils.set_timeout(win.focus_group, 0, 0) swap_regions(view) return view.show(regions[0]) return utils.set_timeout(dont_crash_sublime, 0) msg.debug('View not in group... cloning') win.focus_view(view) def on_clone(buf, view): msg.debug('on clone') def poll_for_move(): msg.debug('poll_for_move') win.focus_view(view) win.set_view_index(view, win.num_groups() - 1, 0) if not get_view_in_group(view.buffer_id(), focus_group): return utils.set_timeout(poll_for_move, 20) msg.debug('found view, now moving ', view.name(), win.num_groups() - 1) swap_regions(view) view.show(regions[0]) win.focus_view(view) utils.set_timeout(win.focus_group, 0, 0) try: del self.temp_ignore_highlight[buf_id] except Exception: pass utils.set_timeout(win.focus_group, 0, 0) poll_for_move() self.on_clone[buf_id] = on_clone self.temp_ignore_highlight[buf_id] = True win.run_command('clone_file') return win.focus_group(0)
def _on_share_dir(self, data): file_to_share = None utils.reload_settings() G.USERNAME = data['username'] G.SECRET = data['secret'] dir_to_share = data['dir_to_share'] perms = data['perms'] editor.line_endings = data['line_endings'].find("unix") >= 0 and "\n" or "\r\n" dir_to_share = os.path.expanduser(dir_to_share) dir_to_share = utils.unfuck_path(dir_to_share) workspace_name = os.path.basename(dir_to_share) G.PROJECT_PATH = os.path.realpath(dir_to_share) msg.debug('%s %s %s' % (G.USERNAME, workspace_name, G.PROJECT_PATH)) if os.path.isfile(dir_to_share): file_to_share = dir_to_share dir_to_share = os.path.dirname(dir_to_share) try: utils.mkdir(dir_to_share) except Exception: return msg.error("The directory %s doesn't exist and I can't create it." % dir_to_share) floo_file = os.path.join(dir_to_share, '.floo') info = {} try: floo_info = open(floo_file, 'rb').read().decode('utf-8') info = json.loads(floo_info) except (IOError, OSError): pass except Exception as e: msg.warn("Couldn't read .floo file: %s: %s" % (floo_file, str(e))) workspace_url = info.get('url') if workspace_url: parsed_url = api.prejoin_workspace(workspace_url, dir_to_share, {'perms': perms}) if parsed_url: # TODO: make sure we create_flooignore # utils.add_workspace_to_persistent_json(parsed_url['owner'], parsed_url['workspace'], workspace_url, dir_to_share) agent = self.remote_connect(parsed_url['owner'], parsed_url['workspace'], False) return agent.once("room_info", lambda: agent.upload(file_to_share or dir_to_share)) parsed_url = utils.get_workspace_by_path(dir_to_share, lambda workspace_url: api.prejoin_workspace(workspace_url, dir_to_share, {'perms': perms})) if parsed_url: agent = self.remote_connect(parsed_url['owner'], parsed_url['workspace'], False) return agent.once("room_info", lambda: agent.upload(file_to_share or dir_to_share)) def on_done(data, choices=None): self.get_input('Workspace name:', workspace_name, self._on_create_workspace, workspace_name, dir_to_share, owner=data.get('response'), perms=perms) try: r = api.get_orgs_can_admin() except IOError as e: return editor.error_message('Error getting org list: %s' % str(e)) if r.code >= 400 or len(r.body) == 0: return on_done({'response': G.USERNAME}) i = 0 choices = [] choices.append([G.USERNAME, i]) for org in r.body: i += 1 choices.append([org['name'], i]) self.get_input('Create workspace owned by (%s) ' % " ".join([x[0] for x in choices]), '', on_done, choices=choices)
def _on_share_dir(self, data): file_to_share = None utils.reload_settings() G.USERNAME = data['username'] G.SECRET = data['secret'] dir_to_share = data['dir_to_share'] perms = data['perms'] editor.line_endings = data['line_endings'].find( "unix") >= 0 and "\n" or "\r\n" dir_to_share = os.path.expanduser(dir_to_share) dir_to_share = utils.unfuck_path(dir_to_share) workspace_name = os.path.basename(dir_to_share) G.PROJECT_PATH = os.path.realpath(dir_to_share) msg.debug('%s %s %s' % (G.USERNAME, workspace_name, G.PROJECT_PATH)) if os.path.isfile(dir_to_share): file_to_share = dir_to_share dir_to_share = os.path.dirname(dir_to_share) try: utils.mkdir(dir_to_share) except Exception: return msg.error( "The directory %s doesn't exist and I can't create it." % dir_to_share) floo_file = os.path.join(dir_to_share, '.floo') info = {} try: floo_info = open(floo_file, 'rb').read().decode('utf-8') info = json.loads(floo_info) except (IOError, OSError): pass except Exception as e: msg.warn("Couldn't read .floo file: %s: %s" % (floo_file, str(e))) workspace_url = info.get('url') if workspace_url: parsed_url = api.prejoin_workspace(workspace_url, dir_to_share, {'perms': perms}) if parsed_url: # TODO: make sure we create_flooignore # utils.add_workspace_to_persistent_json(parsed_url['owner'], parsed_url['workspace'], workspace_url, dir_to_share) agent = self.remote_connect(parsed_url['owner'], parsed_url['workspace'], False) return agent.once( "room_info", lambda: agent.upload(file_to_share or dir_to_share)) parsed_url = utils.get_workspace_by_path( dir_to_share, lambda workspace_url: api.prejoin_workspace( workspace_url, dir_to_share, {'perms': perms})) if parsed_url: agent = self.remote_connect(parsed_url['owner'], parsed_url['workspace'], False) return agent.once( "room_info", lambda: agent.upload(file_to_share or dir_to_share)) def on_done(data, choices=None): self.get_input('Workspace name:', workspace_name, self._on_create_workspace, workspace_name, dir_to_share, owner=data.get('response'), perms=perms) try: r = api.get_orgs_can_admin() except IOError as e: return editor.error_message('Error getting org list: %s' % str(e)) if r.code >= 400 or len(r.body) == 0: return on_done({'response': G.USERNAME}) i = 0 choices = [] choices.append([G.USERNAME, i]) for org in r.body: i += 1 choices.append([org['name'], i]) self.get_input('Create workspace owned by (%s) ' % " ".join([x[0] for x in choices]), '', on_done, choices=choices)
def list_messages(self): if not G.AGENT: return msg.warn('Not connected to a workspace.') self.vim.command('echom "Recent messages for %s"' % (G.AGENT.workspace,)) for message in G.AGENT.get_messages(): self.vim.command('echom " %s"' % (message,))
def on_patch(self, data): added_newline = False buf_id = data['id'] buf = self.FLOO_BUFS[buf_id] view = self.get_view(buf_id) if len(data['patch']) == 0: msg.error('wtf? no patches to apply. server is being stupid') return dmp_patches = DMP.patch_fromText(data['patch']) # TODO: run this in a separate thread if view: old_text = view.get_text() else: old_text = buf.get('buf', '') md5_before = hashlib.md5(old_text.encode('utf-8')).hexdigest() if md5_before != data['md5_before']: msg.debug('maybe vim is lame and discarded a trailing newline') old_text += '\n' added_newline = True md5_before = hashlib.md5(old_text.encode('utf-8')).hexdigest() if md5_before != data['md5_before']: msg.warn('starting md5s don\'t match for %s. ours: %s patch: %s this is dangerous!' % (buf['path'], md5_before, data['md5_before'])) if added_newline: old_text = old_text[:-1] md5_before = hashlib.md5(old_text.encode('utf-8')).hexdigest() t = DMP.patch_apply(dmp_patches, old_text) clean_patch = True for applied_patch in t[1]: if not applied_patch: clean_patch = False break if G.DEBUG: if len(t[0]) == 0: msg.debug('OMG EMPTY!') msg.debug('Starting data:', buf['buf']) msg.debug('Patch:', data['patch']) if '\x01' in t[0]: msg.debug('FOUND CRAZY BYTE IN BUFFER') msg.debug('Starting data:', buf['buf']) msg.debug('Patch:', data['patch']) if not clean_patch: msg.error('failed to patch %s cleanly. re-fetching buffer' % buf['path']) return self.agent.send_get_buf(buf_id) cur_hash = hashlib.md5(t[0].encode('utf-8')).hexdigest() if cur_hash != data['md5_after']: msg.warn( '%s new hash %s != expected %s. re-fetching buffer...' % (buf['path'], cur_hash, data['md5_after']) ) return self.agent.send_get_buf(buf_id) buf['buf'] = t[0] buf['md5'] = cur_hash if not view: self.save_buf(buf) return view.apply_patches(buf, t)
def on_patch(self, data): if len(data['patch']) == 0: msg.error('wtf? no patches to apply. server is being stupid') return buf_id = data['id'] buf = self.FLOO_BUFS[buf_id] if buf['encoding'] == 'base64': # TODO apply binary patches return self.agent.send_get_buf(buf_id) view = self.get_view(buf_id) dmp_patches = DMP.patch_fromText(data['patch']) # TODO: run this in a separate thread if view: old_text = view.get_text() else: old_text = buf.get('buf', '') md5_before = hashlib.md5(old_text.encode('utf-8')).hexdigest() if md5_before != data['md5_before']: msg.debug('maybe vim is lame and discarded a trailing newline') old_text += '\n' md5_before = hashlib.md5(old_text.encode('utf-8')).hexdigest() if md5_before != data['md5_before']: msg.warn('starting md5s don\'t match for %s. ours: %s patch: %s this is dangerous!' % (buf['path'], md5_before, data['md5_before'])) t = DMP.patch_apply(dmp_patches, old_text) clean_patch = True for applied_patch in t[1]: if not applied_patch: clean_patch = False break if G.DEBUG: if len(t[0]) == 0: msg.debug('OMG EMPTY!') msg.debug('Starting data:', buf['buf']) msg.debug('Patch:', data['patch']) if '\x01' in t[0]: msg.debug('FOUND CRAZY BYTE IN BUFFER') msg.debug('Starting data:', buf['buf']) msg.debug('Patch:', data['patch']) if not clean_patch: msg.error('failed to patch %s cleanly. re-fetching buffer' % buf['path']) return self.agent.send_get_buf(buf_id) cur_hash = hashlib.md5(t[0].encode('utf-8')).hexdigest() if cur_hash != data['md5_after']: msg.warn( '%s new hash %s != expected %s. re-fetching buffer...' % (buf['path'], cur_hash, data['md5_after']) ) return self.agent.send_get_buf(buf_id) buf['buf'] = t[0] buf['md5'] = cur_hash if not view: self.save_buf(buf) return view.apply_patches(buf, t)