├── .gitignore ├── collab ├── __init__.py ├── server.py ├── client.py ├── optransform.py ├── model.py ├── doc.py ├── session.py └── connection.py ├── Default (Windows).sublime-keymap ├── Default (Linux).sublime-keymap ├── Default (OSX).sublime-keymap ├── Default.sublime-commands ├── run_server.py ├── license.txt ├── readme.md └── collaboration.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc -------------------------------------------------------------------------------- /collab/__init__.py: -------------------------------------------------------------------------------- 1 | import client, server -------------------------------------------------------------------------------- /Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["ctrl+alt+c"], "command": "collab_connect_to_server" }, 3 | { "keys": ["ctrl+alt+d"], "command": "collab_disconnect_from_server" }, 4 | { "keys": ["ctrl+alt+o"], "command": "collab_open_document" }, 5 | { "keys": ["ctrl+alt+s"], "command": "collab_toggle_server" }, 6 | { "keys": ["ctrl+alt+a"], "command": "collab_add_current_document" } 7 | ] -------------------------------------------------------------------------------- /Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["ctrl+alt+c"], "command": "collab_connect_to_server" }, 3 | { "keys": ["ctrl+alt+d"], "command": "collab_disconnect_from_server" }, 4 | { "keys": ["ctrl+alt+o"], "command": "collab_open_document" }, 5 | { "keys": ["ctrl+alt+s"], "command": "collab_toggle_server" }, 6 | { "keys": ["ctrl+alt+a"], "command": "collab_add_current_document" } 7 | ] -------------------------------------------------------------------------------- /Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["command+alt+c"], "command": "collab_connect_to_server" }, 3 | { "keys": ["command+alt+d"], "command": "collab_disconnect_from_server" }, 4 | { "keys": ["command+alt+o"], "command": "collab_open_document" }, 5 | { "keys": ["command+alt+s"], "command": "collab_toggle_server" }, 6 | { "keys": ["command+alt+a"], "command": "collab_add_current_document" } 7 | ] -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Collaboration: Connect To Server", 4 | "command": "collab_connect_to_server" 5 | }, 6 | { 7 | "caption": "Collaboration: Disconnect From Server", 8 | "command": "collab_disconnect_from_server" 9 | }, 10 | { 11 | "caption": "Collaboration: Toggle Local Server", 12 | "command": "collab_toggle_server" 13 | }, 14 | { 15 | "caption": "Collaboration: Open Document", 16 | "command": "collab_open_document" 17 | }, 18 | { 19 | "caption": "Collaboration: Add Current Document", 20 | "command": "collab_add_current_document" 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /collab/server.py: -------------------------------------------------------------------------------- 1 | import threading, session, model, connection 2 | 3 | class CollabServer(object): 4 | def __init__(self, options=None): 5 | if not options: 6 | options = {} 7 | 8 | self.options = options 9 | self.model = model.CollabModel(options) 10 | self.host = self.options.get('host', '127.0.0.1') 11 | self.port = self.options.get('port', 6633) 12 | self.idtrack = 0 13 | 14 | self.server = connection.SocketServer(self.host, self.port) 15 | self.server.on('connection', lambda connection: session.CollabSession(connection, self.model, self.new_id())) 16 | 17 | def run_forever(self): 18 | threading.Thread(target=self.server.run_forever).start() 19 | 20 | def new_id(self): 21 | self.idtrack += 1 22 | return self.idtrack 23 | 24 | def close(self): 25 | self.model.close() 26 | self.server.close() 27 | -------------------------------------------------------------------------------- /run_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import collab 4 | 5 | def main(argv): 6 | if len(argv) == 1: 7 | if ':' not in argv[0]: 8 | sys.stderr.write("please provide `host:port' to bind or just `host:' for default port\n") 9 | return -2 10 | else: 11 | host, port = argv[0].split(':', 1) 12 | elif len(argv) == 2: 13 | host,port = [x.strip() for x in argv] 14 | elif len(argv) > 2: 15 | sys.stderr.write("use `host:port' arguments\n") 16 | return -3 17 | else: 18 | host, port = '', '' 19 | 20 | if host == '': 21 | host = '0.0.0.0' 22 | if port == '': 23 | port = 6633 24 | try: 25 | sys.stderr.write('Starting at ' + host +':' + str(port) + '...') 26 | server = collab.server.CollabServer({'host':host, 'port': int(port)}) 27 | sys.stderr.write(' started.\n') 28 | server.run_forever() 29 | except KeyboardInterrupt: 30 | print("^C received, server stopped") 31 | return -1 32 | 33 | if __name__ == '__main__': 34 | sys.exit(main(sys.argv[1:])) 35 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | Anyone is free to copy, modify, publish, use, compile, sell, or 3 | distribute this software, either in source code form or as a compiled 4 | binary, for any purpose, commercial or non-commercial, and by any 5 | means. 6 | 7 | In jurisdictions that recognize copyright laws, the author or authors 8 | of this software dedicate any and all copyright interest in the 9 | software to the public domain. We make this dedication for the benefit 10 | of the public at large and to the detriment of our heirs and 11 | successors. We intend this dedication to be an overt act of 12 | relinquishment in perpetuity of all present and future rights to this 13 | software under copyright law. 14 | 15 | The software is provided "as is", without warranty of any kind, 16 | express or implied, including but not limited to the warranties of 17 | merchantibility, fitness for a particular purpose and noninfringement. 18 | In no event shall the authors be liable for any claim, damages or 19 | other liability, whether in an action of contract, tort or otherwise, 20 | arising from, out of or in connection with the software or the use or 21 | other dealings in the software. -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Sublime Collaboration 2 | ===================== 3 | 4 | A real-time collaborative editing plugin for Sublime Text 2. 5 | 6 | Instructions 7 | ------------ 8 | Please note that this plugin is in an early beta state, and much of the user interface and API will probably be rewritten at some point, so this is not representative of what the final product will be like. 9 | 10 | ### Install 11 | Put everything in this repo into a folder named Collaboration under the /Data/Packages/ directory in your Sublime Text 2 folder. 12 | 13 | #### Commands 14 | "Collaboration: Connect To Server": Connects to a Sublime Collaboration server. *Default Shortcut: ctrl+alt+c* 15 | "Collaboration: Disconnect From Server": Disconnect from server. *Default Shortcut: ctrl+alt+d* 16 | "Collaboration: Toggle Local Server": Toggles local server on and off. *Default Shortcut: ctrl+alt+s* 17 | "Collaboration: Open Document": Open a preexisting document on the server. *Default Shortcut: ctrl+alt+o* 18 | "Collaboration: Add Current Document": Uploads the currently open document to the server for collaborative editing. *Default Shortcut: ctrl+alt+a* 19 | 20 | #### Example 21 | If you're just testing this out, first toggle on your local server and connect to it. Then open up a new blank document and add it to the server with the "Add Current Document" command. Currently you can't open the same document in the same Sublime Text process, so you'll need to connect to the server again from another computer and open the document to test out the collaborative aspects. It uses port 6633 if you need to make firewall rules. If all goes well, you should see changes in one buffer replicated on the other. 22 | 23 | #### Bugs 24 | If you find something that creates an error or doesn't seem to be working properly, please make a GitHub issue about it. There are bound to be errors that I don't catch, so any feedback would be appreciated! 25 | -------------------------------------------------------------------------------- /collab/client.py: -------------------------------------------------------------------------------- 1 | import logging, doc, connection 2 | 3 | class CollabClient: 4 | def __init__(self, host, port): 5 | self.docs = {} 6 | self.state = 'connecting' 7 | 8 | self.waiting_for_docs = [] 9 | 10 | self.connected = False 11 | self.id = None 12 | 13 | self.socket = connection.ClientSocket(host, port) 14 | self.socket.on('message', self.socket_message) 15 | self.socket.on('error', self.socket_error) 16 | self.socket.on('open', self.socket_open) 17 | self.socket.on('close', self.socket_close) 18 | self.socket.start() 19 | 20 | self._events = {} 21 | 22 | def on(self, event, fct): 23 | if event not in self._events: self._events[event] = [] 24 | self._events[event].append(fct) 25 | return self 26 | 27 | def removeListener(self, event, fct): 28 | if event not in self._events: return self 29 | self._events[event].remove(fct) 30 | return self 31 | 32 | def emit(self, event, *args): 33 | if event not in self._events: return self 34 | for callback in self._events[event]: 35 | callback(*args) 36 | return self 37 | 38 | def socket_open(self): 39 | self.set_state('handshaking') 40 | 41 | def socket_close(self, reason=''): 42 | self.set_state('closed', reason) 43 | self.socket = None 44 | 45 | def socket_error(self, error): 46 | self.emit('error', error) 47 | 48 | def socket_message(self, msg): 49 | if 'auth' in msg: 50 | if msg['auth'] is None or msg['auth'] == '': 51 | logging.warning('Authentication failed: {0}'.format(msg['error'])) 52 | self.disconnect() 53 | else: 54 | self.id = msg['auth'] 55 | self.set_state('ok') 56 | return 57 | 58 | if 'docs' in msg: 59 | if 'error' in msg: 60 | for callback in self.waiting_for_docs: 61 | callback(msg['error'], None) 62 | else: 63 | for callback in self.waiting_for_docs: 64 | callback(None, msg['docs']) 65 | self.waiting_for_docs = [] 66 | return 67 | 68 | if 'doc' in msg and msg['doc'] in self.docs: 69 | self.docs[msg['doc']].on_message(msg) 70 | else: 71 | logging.error('Unhandled message {0}'.format(msg)) 72 | 73 | def set_state(self, state, data=None): 74 | if self.state is state: return 75 | self.state = state 76 | 77 | if state is 'closed': 78 | self.id = None 79 | self.emit(state, data) 80 | 81 | def send(self, data): 82 | if self.state is not "closed": 83 | self.socket.send(data) 84 | 85 | def disconnect(self): 86 | if self.state is not "closed": 87 | self.socket.close() 88 | 89 | def get_docs(self, callback): 90 | if self.state is 'closed': 91 | return callback('connection closed', None) 92 | 93 | if self.state is 'connecting': 94 | return self.on('ok', lambda x: self.get_docs(callback)) 95 | 96 | if not self.waiting_for_docs: 97 | self.send({"docs":None}) 98 | self.waiting_for_docs.append(callback) 99 | 100 | def open(self, name, callback, **kwargs): 101 | if self.state is 'closed': 102 | return callback('connection closed', None) 103 | 104 | if self.state is 'connecting': 105 | return self.on('ok', lambda x: self.open(name, callback)) 106 | 107 | if name in self.docs: 108 | return callback("doc {0} already open".format(name), None) 109 | 110 | newdoc = doc.CollabDoc(self, name, kwargs.get('snapshot', None)) 111 | self.docs[name] = newdoc 112 | 113 | newdoc.open(lambda error, doc: callback(error, doc if not error else None)) 114 | 115 | def closed(self, name): 116 | del self.docs[name] 117 | 118 | -------------------------------------------------------------------------------- /collab/optransform.py: -------------------------------------------------------------------------------- 1 | def inject(s1, pos, s2): 2 | return s1[:pos] + s2 + s1[pos:] 3 | 4 | def apply(snapshot, op): 5 | for component in op: 6 | if 'i' in component: 7 | snapshot = inject(snapshot, component['p'], component['i']) 8 | else: 9 | deleted = snapshot[component['p']:(component['p'] + len(component['d']))] 10 | if(component['d'] != deleted): raise Exception("Delete component '{0}' does not match deleted text '{1}'".format(component['d'], deleted)) 11 | snapshot = snapshot[:component['p']] + snapshot[(component['p'] + len(component['d'])):] 12 | return snapshot 13 | 14 | def append(newOp, c): 15 | if 'i' in c and c['i'] == '': return 16 | if 'd' in c and c['d'] == '': return 17 | if len(newOp) == 0: 18 | newOp.append(c) 19 | else: 20 | last = newOp[len(newOp) - 1] 21 | 22 | if 'i' in last and 'i' in c and last['p'] <= c['p'] <= (last['p'] + len(last['i'])): 23 | newOp[len(newOp) - 1] = {'i':inject(last['i'], c['p'] - last['p'], c['i']), 'p':last['p']} 24 | elif 'd' in last and 'd' in c and c['p'] <= last['p'] <= (c['p'] + len(c['d'])): 25 | newOp[len(newOp) - 1] = {'d':inject(c['d'], last['p'] - c['p'], last['d']), 'p':c['p']} 26 | else: 27 | newOp.append(c) 28 | 29 | def compose(op1, op2): 30 | newOp = list(op1) 31 | [append(newOp, c) for c in op2] 32 | return newOp 33 | 34 | def compress(op): 35 | return compose([], op) 36 | 37 | def normalize(op): 38 | newOp = [] 39 | 40 | if(isinstance(op, dict)): op = [op] 41 | 42 | for c in op: 43 | if 'p' not in c or not c['p']: c['p'] = 0 44 | append(newOp, c) 45 | 46 | return newOp 47 | 48 | def invert_component(c): 49 | if 'i' in c: 50 | return {'d':c['i'], 'p':c['p']} 51 | else: 52 | return {'i':c['d'], 'p':c['p']} 53 | 54 | def invert(op): 55 | return [invert_component(c) for c in reversed(op)] 56 | 57 | def transform_position(pos, c, insertAfter=False): 58 | if 'i' in c: 59 | if c['p'] < pos or (c['p'] == pos and insertAfter): 60 | return pos + len(c['i']) 61 | else: 62 | return pos 63 | else: 64 | if pos <= c['p']: 65 | return pos 66 | elif pos <= c['p'] + len(c['d']): 67 | return c['p'] 68 | else: 69 | return pos - len(c['d']) 70 | 71 | def transform_cursor(position, op, insertAfter=False): 72 | for c in op: 73 | position = transform_position(position, c, insertAfter) 74 | return position 75 | 76 | def transform_component(dest, c, otherC, type): 77 | if 'i' in c: 78 | append(dest, {'i':c['i'], 'p':transform_position(c['p'], otherC, type == 'right')}) 79 | else: 80 | if 'i' in otherC: 81 | s = c['d'] 82 | if c['p'] < otherC['p']: 83 | append(dest, {'d':s[:otherC['p'] - c['p']], 'p':c['p']}) 84 | s = s[(otherC['p'] - c['p']):] 85 | pass 86 | if s != '': 87 | append(dest, {'d':s, 'p':c['p'] + len(otherC['i'])}) 88 | else: 89 | if c['p'] >= otherC['p'] + len(otherC['d']): 90 | append(dest, {'d':c['d'], 'p':c['p'] - len(otherC['d'])}) 91 | elif c['p'] + len(c['d']) <= otherC['p']: 92 | append(dest, c) 93 | else: 94 | newC = {'d':'', 'p':c['p']} 95 | if c['p'] < otherC['p']: 96 | newC['d'] = c['d'][:(otherC['p'] - c['p'])] 97 | pass 98 | if c['p'] + len(c['d']) > otherC['p'] + len(otherC['d']): 99 | newC['d'] += c['d'][(otherC['p'] + len(otherC['d']) - c['p']):] 100 | pass 101 | 102 | intersectStart = max(c['p'], otherC['p']) 103 | intersectEnd = min(c['p'] + len(c['d']), otherC['p'] + len(otherC['d'])) 104 | cIntersect = c['d'][intersectStart - c['p']:intersectEnd - c['p']] 105 | otherIntersect = otherC['d'][intersectStart - otherC['p']:intersectEnd - otherC['p']] 106 | if cIntersect != otherIntersect: 107 | raise Exception('Delete ops delete different text in the same region of the document') 108 | 109 | if newC['d'] != '': 110 | newC['p'] = transform_position(newC['p'], otherC) 111 | append(dest, newC) 112 | 113 | return dest 114 | 115 | def transform_component_x(left, right, destLeft, destRight): 116 | transform_component(destLeft, left, right, 'left') 117 | transform_component(destRight, right, left, 'right') 118 | 119 | def transform_x(leftOp, rightOp): 120 | newRightOp = [] 121 | 122 | for rightComponent in rightOp: 123 | newLeftOp = [] 124 | 125 | k = 0 126 | while k < len(leftOp): 127 | nextC = [] 128 | transform_component_x(leftOp[k], rightComponent, newLeftOp, nextC) 129 | k+=1 130 | 131 | if len(nextC) == 1: 132 | rightComponent = nextC[0] 133 | elif len(nextC) == 0: 134 | [append(newLeftOp, l) for l in leftOp[k:]] 135 | rightComponent = None 136 | break 137 | else: 138 | l_, r_ = transform_x(leftOp[k:], nextC) 139 | [append(newLeftOp, l) for l in l_] 140 | [append(newRightOp, r) for r in r_] 141 | rightComponent = None 142 | break 143 | 144 | if rightComponent: 145 | append(newRightOp, rightComponent) 146 | leftOp = newLeftOp 147 | 148 | return [leftOp, newRightOp] 149 | 150 | def transform(op, otherOp, side): 151 | if side != 'left' and side != 'right': 152 | raise ValueError("side must be 'left' or 'right'") 153 | 154 | if len(otherOp) == 0: 155 | return op 156 | 157 | if len(op) == 1 and len(otherOp) == 1: 158 | return transform_component([], op[0], otherOp[0], side) 159 | 160 | if side == 'left': 161 | left, _ = transform_x(op, otherOp) 162 | return left 163 | else: 164 | _, right = transform_x(otherOp, op) 165 | return right 166 | -------------------------------------------------------------------------------- /collab/model.py: -------------------------------------------------------------------------------- 1 | import time, re, logging, optransform 2 | 3 | class CollabModel(object): 4 | def __init__(self, options=None): 5 | self.options = options if options else {} 6 | self.options.setdefault('numCachedOps', 20) 7 | self.options.setdefault('opsBeforeCommit', 20) 8 | self.options.setdefault('maximumAge', 20) 9 | 10 | self.docs = {} 11 | 12 | def process_queue(self, doc): 13 | if doc['queuelock'] or len(doc['queue']) == 0: 14 | return 15 | 16 | doc['queuelock'] = True 17 | op, callback = doc['queue'].pop(0) 18 | self.handle_op(doc, op, callback) 19 | doc['queuelock'] = False 20 | 21 | self.process_queue(doc) 22 | 23 | def handle_op(self, doc, op, callback): 24 | if 'v' not in op or op['v'] < 0: 25 | return callback('Version missing', None) 26 | if op['v'] > doc['v']: 27 | return callback('Op at future version', None) 28 | if op['v'] < doc['v'] - self.options['maximumAge']: 29 | return callback('Op too old', None) 30 | if op['v'] < 0: 31 | return callback('Invalid version', None) 32 | 33 | ops = doc['ops'][(len(doc['ops'])+op['v']-doc['v']):] 34 | 35 | if doc['v'] - op['v'] != len(ops): 36 | logging.error("Could not get old ops in model for document {1}. Expected ops {1} to {2} and got {3} ops".format(doc['name'], op['v'], doc['v'], len(ops))) 37 | return callback('Internal error', None) 38 | 39 | for oldOp in ops: 40 | op['op'] = optransform.transform(op['op'], oldOp['op'], 'left') 41 | op['v']+=1 42 | 43 | newSnapshot = optransform.apply(doc['snapshot'], op['op']) 44 | 45 | if op['v'] != doc['v']: 46 | logging.error("Version mismatch detected in model. File a ticket - this is a bug. Expecting {0} == {1}".format(op['v'], doc['v'])) 47 | return callback('Internal error', None) 48 | 49 | oldSnapshot = doc['snapshot'] 50 | doc['v'] = op['v'] + 1 51 | doc['snapshot'] = newSnapshot 52 | for listener in doc['listeners']: 53 | listener(op, newSnapshot, oldSnapshot) 54 | 55 | def save_op_callback(error=None): 56 | if error: 57 | logging.error("Error saving op: {0}".format(error)) 58 | return callback(error, None) 59 | else: 60 | return callback(None, op['v']) 61 | self.save_op(doc['name'], op, save_op_callback) 62 | 63 | def save_op(self, docname, op, callback): 64 | doc = self.docs[docname] 65 | doc['ops'].append(op) 66 | if len(doc['ops']) > self.options['numCachedOps']: 67 | doc['ops'].pop(0) 68 | if not doc['savelock'] and doc['savedversion'] + self.options['opsBeforeCommit'] <= doc['v']: 69 | pass 70 | callback(None) 71 | 72 | def exists(self, docname): 73 | return docname in self.docs 74 | 75 | def get_docs(self, callback): 76 | callback(None, [self.docs[doc]['name'] for doc in self.docs]) 77 | 78 | def add(self, docname, data): 79 | self.docs[docname] = { 80 | 'name': docname, 81 | 'snapshot': data['snapshot'], 82 | 'v': data['v'], 83 | 'ops': data['ops'], 84 | 'listeners': [], 85 | 'savelock': False, 86 | 'savedversion': 0, 87 | 'queue': [], 88 | 'queuelock': False, 89 | } 90 | 91 | def load(self, docname, callback): 92 | try: 93 | return callback(None, self.docs[docname]) 94 | except KeyError: 95 | return callback('Document does not exist', None) 96 | 97 | # self.loadingdocs = {} 98 | # self.loadingdocs.setdefault(docname, []).append(callback) 99 | # if docname in self.loadingdocs: 100 | # for callback in self.loadingdocs[docname]: 101 | # callback(None, doc) 102 | # del self.loadingdocs[docname] 103 | 104 | def create(self, docname, snapshot=None, callback=None): 105 | if not re.match("^[A-Za-z0-9._-]*$", docname): 106 | return callback('Invalid document name') if callback else None 107 | if self.exists(docname): 108 | return callback('Document already exists') if callback else None 109 | 110 | data = { 111 | 'snapshot': snapshot if snapshot else '', 112 | 'v': 0, 113 | 'ops': [] 114 | } 115 | self.add(docname, data) 116 | 117 | return callback(None) if callback else None 118 | 119 | def delete(self, docname, callback=None): 120 | if docname not in self.docs: raise Exception('delete called but document does not exist') 121 | del self.docs[docname] 122 | return callback(None) if callback else None 123 | 124 | def listen(self, docname, listener, callback=None): 125 | def done(error, doc): 126 | if error: return callback(error, None) if callback else None 127 | doc['listeners'].append(listener) 128 | return callback(None, doc['v']) if callback else None 129 | self.load(docname, done) 130 | 131 | def remove_listener(self, docname, listener): 132 | if docname not in self.docs: raise Exception('remove_listener called but document not loaded') 133 | self.docs[docname]['listeners'].remove(listener) 134 | 135 | def get_version(self, docname, callback): 136 | self.load(docname, lambda error, doc: callback(error, None if error else doc['v'])) 137 | 138 | def get_snapshot(self, docname, callback): 139 | self.load(docname, lambda error, doc: callback(error, None if error else doc['snapshot'])) 140 | 141 | def get_data(self, docname, callback): 142 | self.load(docname, lambda error, doc: callback(error, None if error else doc)) 143 | 144 | def apply_op(self, docname, op, callback): 145 | def on_load(error, doc): 146 | if error: 147 | callback(error, None) 148 | else: 149 | doc['queue'].append((op, callback)) 150 | self.process_queue(doc) 151 | self.load(docname, on_load) 152 | 153 | def flush(self, callback=None): 154 | return callback() if callback else None 155 | 156 | def close(self): 157 | self.flush() 158 | -------------------------------------------------------------------------------- /collab/doc.py: -------------------------------------------------------------------------------- 1 | import functools, optransform 2 | 3 | class CollabDoc(): 4 | def __init__(self, connection, name, snapshot=None): 5 | self.connection = connection 6 | self.name = name 7 | self.version = 0 8 | self.snapshot = snapshot 9 | self.state = 'closed' 10 | 11 | self._events = {} 12 | 13 | self.on('remoteop', self.on_doc_remoteop) 14 | 15 | self.connection.on('closed', lambda data: self.set_state('closed', data)) 16 | 17 | self.inflight_op = None 18 | self.inflight_callbacks = [] 19 | self.pending_op = None 20 | self.pending_callbacks = [] 21 | self.server_ops = {} 22 | 23 | self._open_callback = None 24 | 25 | def on(self, event, fct): 26 | if event not in self._events: self._events[event] = [] 27 | self._events[event].append(fct) 28 | return self 29 | 30 | def removeListener(self, event, fct): 31 | if event not in self._events: return self 32 | self._events[event].remove(fct) 33 | return self 34 | 35 | def emit(self, event, *args): 36 | if event not in self._events: return self 37 | for callback in self._events[event]: 38 | callback(*args) 39 | return self 40 | 41 | def set_state(self, state, data=None): 42 | if self.state is state: return 43 | self.state = state 44 | 45 | if state is 'closed': 46 | if self._open_callback: self._open_callback(data if data else "disconnected", None) 47 | 48 | self.emit(state, data) 49 | 50 | def __len__(self): 51 | return len(self.snapshot) 52 | 53 | def get_text(self): 54 | return self.snapshot 55 | 56 | def insert(self, pos, text, callback=None): 57 | op = [{'p':pos, 'i':text}] 58 | self.submit_op(op, callback) 59 | return op 60 | 61 | def delete(self, pos, length, callback=None): 62 | op = [{'p':pos, 'd':self.snapshot[pos:(pos+length)]}] 63 | self.submit_op(op, callback) 64 | return op 65 | 66 | def on_doc_remoteop(self, op, snapshot): 67 | for component in op: 68 | if 'i' in component: 69 | self.emit('insert', component['p'], component['i']) 70 | else: 71 | self.emit('delete', component['p'], component['d']) 72 | 73 | def open(self, callback=None): 74 | if self.state != 'closed': return 75 | 76 | self.connection.send({'doc': self.name, 'open': True, 'snapshot': self.snapshot, 'create': True}) 77 | self.set_state('opening') 78 | 79 | self._open_callback = callback 80 | 81 | def close(self): 82 | self.connection.send({'doc':self.name, 'open':False}) 83 | self.set_state('closed', 'closed by local client') 84 | 85 | def submit_op(self, op, callback): 86 | op = optransform.normalize(op) 87 | self.snapshot = optransform.apply(self.snapshot, op) 88 | 89 | if self.pending_op is not None: 90 | self.pending_op = optransform.compose(self.pending_op, op) 91 | else: 92 | self.pending_op = op 93 | 94 | if callback: 95 | self.pending_callbacks.append(callback) 96 | 97 | self.emit('change', op) 98 | 99 | self.flush() 100 | 101 | def flush(self): 102 | if not (self.connection.state == 'ok' and self.inflight_op is None and self.pending_op is not None): 103 | return 104 | 105 | self.inflight_op = self.pending_op 106 | self.inflight_callbacks = self.pending_callbacks 107 | 108 | self.pending_op = None 109 | self.pending_callbacks = [] 110 | 111 | self.connection.send({'doc':self.name, 'op':self.inflight_op, 'v':self.version}) 112 | 113 | def apply_op(self, op, is_remote): 114 | oldSnapshot = self.snapshot 115 | self.snapshot = optransform.apply(self.snapshot, op) 116 | 117 | self.emit('change', op, oldSnapshot) 118 | if is_remote: 119 | self.emit('remoteop', op, oldSnapshot) 120 | 121 | def on_message(self, msg): 122 | if msg['doc'] != self.name: 123 | return self.emit('error', "Expected docName '{0}' but got {1}".format(self.name, msg['doc'])) 124 | 125 | if 'open' in msg: 126 | if msg['open'] == True: 127 | 128 | if 'create' in msg and msg['create'] and not self.snapshot: 129 | self.snapshot = '' 130 | else: 131 | if 'snapshot' in msg: 132 | self.snapshot = msg['snapshot'] 133 | 134 | if 'v' in msg: 135 | self.version = msg['v'] 136 | 137 | self.state = 'open' 138 | self.emit('open') 139 | 140 | if self._open_callback: 141 | self._open_callback(None, self) 142 | self._open_callback = None 143 | 144 | elif msg['open'] == False: 145 | if 'error' in msg: 146 | self.emit('error', msg['error']) 147 | if self._open_callback: 148 | self._open_callback(msg['error'], None) 149 | self._open_callback = None 150 | 151 | self.set_state('closed', 'closed by remote server') 152 | self.connection.closed(self.name) 153 | 154 | elif 'op' not in msg and 'v' in msg: 155 | if msg['v'] != self.version: 156 | return self.emit('error', "Expected version {0} but got {1}".format(self.version, msg['v'])) 157 | 158 | oldinflight_op = self.inflight_op 159 | self.inflight_op = None 160 | 161 | if 'error' in msg: 162 | error = msg['error'] 163 | undo = optransform.invert(oldinflight_op) 164 | if self.pending_op: 165 | self.pending_op, undo = optransform.transform_x(self.pending_op, undo) 166 | for callback in self.inflight_callbacks: 167 | callback(error, None) 168 | else: 169 | self.server_ops[self.version] = oldinflight_op 170 | self.version += 1 171 | for callback in self.inflight_callbacks: 172 | callback(None, oldinflight_op) 173 | 174 | self.flush() 175 | 176 | elif 'op' in msg and 'v' in msg: 177 | if msg['v'] != self.version: 178 | return self.emit('error', "Expected version {0} but got {1}".format(self.version, msg['v'])) 179 | 180 | op = msg['op'] 181 | self.server_ops[self.version] = op 182 | 183 | if self.inflight_op is not None: 184 | self.inflight_op, op = optransform.transform_x(self.inflight_op, op) 185 | if self.pending_op is not None: 186 | self.pending_op, op = optransform.transform_x(self.pending_op, op) 187 | 188 | self.version += 1 189 | self.apply_op(op, True) 190 | else: 191 | logging.error('Unhandled document message: {0}'.format(msg)) 192 | -------------------------------------------------------------------------------- /collab/session.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | class CollabSession(object): 4 | def __init__(self, connection, model, id): 5 | self.connection = connection 6 | self.model = model 7 | 8 | self.docs = {} 9 | self.userid = id 10 | 11 | if self.connection.ready(): 12 | self.on_session_create() 13 | else: 14 | self.connection.on('ok', lambda: self.on_session_create) 15 | self.connection.on('close', self.on_session_close) 16 | self.connection.on('message', self.on_session_message) 17 | 18 | def on_session_create(self): 19 | self.connection.send({'auth':self.userid}) 20 | 21 | def on_session_close(self): 22 | for docname in self.docs: 23 | if 'listener' in self.docs[docname]: 24 | self.model.remove_listener(docname, self.docs[docname]['listener']) 25 | self.docs = None 26 | 27 | def on_session_message(self, query, callback=None): 28 | if 'docs' in query: 29 | return self.model.get_docs(lambda e, docs: self.on_get_docs(e, docs, callback)) 30 | 31 | error = None 32 | if 'doc' not in query or not isinstance(query['doc'], (str, unicode)): 33 | error = 'doc name invalid or missing' 34 | if 'create' in query and query['create'] is not True: 35 | error = "'create' must be True or missing" 36 | if 'open' in query and query['open'] not in [True, False]: 37 | error = "'open' must be True, False or missing" 38 | if 'v' in query and (not isinstance(query['v'], (int, float)) or query['v'] < 0): 39 | error = "'v' invalid" 40 | 41 | if error: 42 | logging.error("Invalid query {0} from {1}: {2}".format(query, self.userid, error)) 43 | self.connection.abort() 44 | return callback() if callback else None 45 | 46 | if query['doc'] not in self.docs: 47 | self.docs[query['doc']] = {'queue': [], 'queuelock': False} 48 | 49 | doc = self.docs[query['doc']] 50 | doc['queue'].append((query, callback)) 51 | self.process_queue(doc) 52 | 53 | def on_get_docs(self, error, docs, callback): 54 | self.send({"docs":docs} if not error else {"docs":None, "error":error}) 55 | return callback() if callback else None 56 | 57 | def process_queue(self, doc): 58 | if doc['queuelock'] or len(doc['queue']) == 0: 59 | return 60 | 61 | doc['queuelock'] = True 62 | query, callback = doc['queue'].pop(0) 63 | self.handle_message(query, callback) 64 | doc['queuelock'] = False 65 | 66 | self.process_queue(doc) 67 | 68 | def handle_message(self, query, callback = None): 69 | if not self.docs: 70 | return callback() if callback else None 71 | 72 | if 'open' in query and query['open'] == False: 73 | if 'listener' not in self.docs[query['doc']]: 74 | self.send({'doc':query['doc'], 'open':False, 'error':'Doc is not open'}) 75 | else: 76 | self.model.remove_listener(query['doc'], self.docs[query['doc']]['listener']) 77 | del self.docs[query['doc']]['listener'] 78 | self.send({'doc':query['doc'], 'open':False}) 79 | return callback() if callback else None 80 | 81 | elif 'open' in query or ('snapshot' in query and query['snapshot'] is None) or 'create' in query: 82 | self.handle_opencreatesnapshot(query, callback) 83 | 84 | elif 'op' in query and 'v' in query: 85 | def apply_op(error, appliedVersion): 86 | self.send({'doc':query['doc'], 'v':None, 'error':error} if error else {'doc':query['doc'], 'v':appliedVersion}) 87 | return callback() if callback else None 88 | self.model.apply_op(query['doc'], {'doc':query['doc'], 'v':query['v'], 'op':query['op'], 'source':self.userid}, apply_op) 89 | 90 | else: 91 | logging.error("Invalid query {0} from {1}".format(query, self.userid)) 92 | self.connection.abort() 93 | return callback() if callback else None 94 | 95 | def on_remote_message(self, message, snapshot, oldsnapshot): 96 | if message['source'] is self.userid: return 97 | self.send(message) 98 | 99 | def send(self, msg): 100 | self.connection.send(msg) 101 | 102 | def handle_opencreatesnapshot(self, query, callback = None): 103 | def finished(message): 104 | if 'error' in message: 105 | if 'create' in query and 'create' not in message: message['create'] = False 106 | if 'snapshot' in query and 'snapshot' not in message: message['snapshot'] = None 107 | if 'open' in query and 'open' not in message: message['open'] = False 108 | self.send(message) 109 | return callback() if callback else None 110 | 111 | def step1Create(message): 112 | if 'create' not in query: 113 | return step2Snapshot(message) 114 | 115 | def model_create(error=None): 116 | if error == 'Document already exists': 117 | message['create'] = False 118 | return step2Snapshot(message) 119 | elif error: 120 | message['create'] = False 121 | message['error'] = error 122 | return finished(message) 123 | else: 124 | message['create'] = True 125 | return step2Snapshot(message) 126 | 127 | self.model.create(query['doc'], query.get('snapshot', None), model_create) 128 | 129 | def step2Snapshot(message): 130 | if 'snapshot' not in query or message['create']: 131 | return step3Open(message) 132 | 133 | def model_get_data(error, data): 134 | if error: 135 | message['snapshot'] = None 136 | message['error'] = error 137 | return finished(message) 138 | message['v'] = data['v'] 139 | message['snapshot'] = data['snapshot'] 140 | return step3Open(message) 141 | 142 | return self.model.get_data(query['doc'], model_get_data) 143 | 144 | def step3Open(message): 145 | if 'open' not in query: 146 | return finished(message) 147 | 148 | doc = self.docs[query['doc']] 149 | if 'listener' in doc: 150 | message['open'] = True 151 | return finished(message) 152 | 153 | doc['listener'] = self.on_remote_message 154 | 155 | def model_listen(error, v): 156 | if error: 157 | del doc['listener'] 158 | message['open'] = False 159 | message['error'] = error 160 | message['open'] = True 161 | if 'v' not in message: message['v'] = v 162 | return finished(message) 163 | self.model.listen(query['doc'], doc['listener'], model_listen) 164 | 165 | step1Create({'doc':query['doc']}) -------------------------------------------------------------------------------- /collab/connection.py: -------------------------------------------------------------------------------- 1 | import json, threading, socket, base64, hashlib 2 | 3 | class ClientSocket(threading.Thread): 4 | def __init__(self, host, port): 5 | threading.Thread.__init__(self) 6 | 7 | self.host = host 8 | self.port = port 9 | self.sock = None 10 | 11 | self.saved_data = '' 12 | self.target_size = None 13 | 14 | self.keep_running = True 15 | 16 | self._events = {} 17 | 18 | def on(self, event, fct): 19 | if event not in self._events: self._events[event] = [] 20 | self._events[event].append(fct) 21 | return self 22 | 23 | def removeListener(self, event, fct): 24 | if event not in self._events: return self 25 | self._events[event].remove(fct) 26 | return self 27 | 28 | def emit(self, event, *args): 29 | if event not in self._events: return self 30 | for callback in self._events[event]: 31 | callback(*args) 32 | return self 33 | 34 | def send(self, data): 35 | #print('Sending:{0}'.format(data)) 36 | msg = json.dumps(data) 37 | 38 | #A pretty terrible hacky framing system, I'll need to come up with a better one soon 39 | try: 40 | self.sock.send(u"0"*(10-len(unicode(len(msg))))+unicode(len(msg))+msg) 41 | except: 42 | self.close() 43 | 44 | def close(self): 45 | self.keep_running = False 46 | if self.sock: 47 | self.sock.shutdown(socket.SHUT_RDWR) 48 | 49 | def run(self): 50 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 51 | try: 52 | self.sock.connect((self.host, self.port)) 53 | except: 54 | self.emit('error', 'could not connect to server') 55 | self.emit('close') 56 | self.sock = None 57 | return 58 | self.emit('open') 59 | while self.keep_running: 60 | try: 61 | data = self.sock.recv(self.target_size if self.target_size else 10) 62 | except: 63 | break 64 | if data is None or data == "": 65 | break 66 | #print('Recieved:{0}'.format(data)) 67 | if self.target_size: 68 | self.saved_data += data 69 | if len(self.saved_data) == self.target_size: 70 | self.emit('message', json.loads(self.saved_data, "utf-8")) 71 | self.saved_data = '' 72 | self.target_size = None 73 | else: 74 | self.target_size = int(data) 75 | 76 | self.sock.close() 77 | self.emit('close') 78 | self.sock = None 79 | 80 | 81 | 82 | class ServerSocket(threading.Thread): 83 | def __init__(self, sock, addr): 84 | threading.Thread.__init__(self) 85 | 86 | self.sock = sock 87 | sock.settimeout(None) 88 | 89 | self.address = addr 90 | self.headers = None 91 | 92 | self.saved_data = '' 93 | self.target_size = None 94 | 95 | self._ready = False 96 | self._events = {} 97 | 98 | def on(self, event, fct): 99 | if event not in self._events: self._events[event] = [] 100 | self._events[event].append(fct) 101 | return self 102 | 103 | def removeListener(self, event, fct): 104 | if event not in self._events: return self 105 | self._events[event].remove(fct) 106 | return self 107 | 108 | def emit(self, event, *args): 109 | if event not in self._events: return self 110 | for callback in self._events[event]: 111 | callback(*args) 112 | return self 113 | 114 | def run(self): 115 | self._ready = True 116 | self.emit('ok') 117 | 118 | while self._ready: 119 | try: 120 | data = self.sock.recv(self.target_size if self.target_size else 10) 121 | except: 122 | break 123 | if data is None or data == "": 124 | break 125 | #print('Server Recieved from {0}:{1}'.format(self.address, data)) 126 | if self.target_size: 127 | self.saved_data += data 128 | if len(self.saved_data) == self.target_size: 129 | self.emit('message', json.loads(self.saved_data, "utf-8")) 130 | self.saved_data = '' 131 | self.target_size = None 132 | else: 133 | self.target_size = int(data) 134 | 135 | self._ready = False 136 | self.emit('close') 137 | self.close() 138 | 139 | def close(self): 140 | self._ready = False 141 | self.sock.shutdown(socket.SHUT_RDWR) 142 | 143 | def send(self, data): 144 | if not self._ready: return 145 | #print('Server Sending to {0}:{1}'.format(self.address, data)) 146 | msg = json.dumps(data) 147 | 148 | #A pretty terrible hacky framing system, I'll need to come up with a better one soon 149 | try: 150 | self.sock.send(u"0"*(10-len(unicode(len(msg))))+unicode(len(msg))+msg) 151 | except: 152 | self.close() 153 | 154 | def ready(self): 155 | return self._ready 156 | 157 | def abort(self): 158 | self.close() 159 | 160 | def stop(self): 161 | self.close() 162 | 163 | 164 | 165 | class SocketServer: 166 | def __init__(self, host='127.0.0.1', port=6633): 167 | self.host = host 168 | self.port = port 169 | self.sock = None 170 | self.keep_running = True 171 | self.closed = False 172 | self.connections = [] 173 | self._events = {} 174 | 175 | def on(self, event, fct): 176 | if event not in self._events: self._events[event] = [] 177 | self._events[event].append(fct) 178 | return self 179 | 180 | def removeListener(self, event, fct): 181 | if event not in self._events: return self 182 | self._events[event].remove(fct) 183 | return self 184 | 185 | def emit(self, event, *args): 186 | if event not in self._events: return self 187 | for callback in self._events[event]: 188 | callback(*args) 189 | return self 190 | 191 | def run_forever(self): 192 | self.sock = socket.socket() 193 | self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 194 | self.sock.settimeout(0.1) 195 | self.sock.bind(('', self.port)) 196 | self.sock.listen(1) 197 | 198 | while self.keep_running: 199 | try: 200 | conn, addr = self.sock.accept() 201 | except socket.timeout: 202 | continue 203 | #print('Connected by {0}'.format(addr)) 204 | connection = ServerSocket(conn, addr) 205 | self.connections.append(connection) 206 | def on_close(): 207 | #print('Disconnected by {0}'.format(addr)) 208 | if connection in self.connections: 209 | self.connections.remove(connection) 210 | connection.on('close', on_close) 211 | connection.start() 212 | self.emit('connection', connection) 213 | 214 | self.closed = True 215 | 216 | def close(self): 217 | self.keep_running = False 218 | for connection in self.connections: 219 | connection.close() 220 | self.sock.close() 221 | while not self.closed: pass 222 | -------------------------------------------------------------------------------- /collaboration.py: -------------------------------------------------------------------------------- 1 | import collab, sublime, sublime_plugin, os 2 | 3 | class SublimeListener(sublime_plugin.EventListener): 4 | _events = {} 5 | 6 | @classmethod 7 | def on(klass, event, fct): 8 | if event not in klass._events: klass._events[event] = [] 9 | klass._events[event].append(fct) 10 | 11 | @classmethod 12 | def removeListener(klass, event, fct): 13 | if event not in klass._events: return 14 | klass._events[event].remove(fct) 15 | 16 | def emit(self, event, *args): 17 | if event not in self._events: return 18 | for callback in self._events[event]: 19 | callback(*args) 20 | return self 21 | 22 | def on_modified(self, view): 23 | self.emit("modified", view) 24 | 25 | def on_new(self, view): 26 | self.emit("new", view) 27 | 28 | def on_clone(self, view): 29 | self.emit("clone", view) 30 | 31 | def on_load(self, view): 32 | self.emit("load", view) 33 | 34 | def on_close(self, view): 35 | self.emit("close", view) 36 | 37 | def on_pre_save(self, view): 38 | self.emit("pre_save", view) 39 | 40 | def on_post_save(self, view): 41 | self.emit("post_save", view) 42 | 43 | def on_selection_modified(self, view): 44 | self.emit("selection_modified", view) 45 | 46 | def on_activated(self, view): 47 | self.emit("activated", view) 48 | 49 | def on_deactivated(self, view): 50 | self.emit("deactivated", view) 51 | 52 | 53 | 54 | class SublimeEditor(object): 55 | def __init__(self, view, doc): 56 | self.doc = None 57 | self.view = view 58 | self.doc = doc 59 | self._events = {} 60 | self.state = "ok" 61 | self.in_remoteop = False 62 | 63 | SublimeListener.on("modified", self._on_view_modified) 64 | SublimeListener.on("close", self._on_view_close) 65 | SublimeListener.on("post_save", self._on_view_post_save) 66 | self.doc.on("closed", self.close) 67 | self.doc.on("remoteop", self._on_doc_remoteop) 68 | 69 | sublime.set_timeout(lambda: self._initialize(self.doc.get_text()), 0) 70 | 71 | print("opened "+doc.name) 72 | 73 | def on(self, event, fct): 74 | if event not in self._events: self._events[event] = [] 75 | self._events[event].append(fct) 76 | return self 77 | 78 | def removeListener(self, event, fct): 79 | if event not in self._events: return self 80 | self._events[event].remove(fct) 81 | return self 82 | 83 | def emit(self, event, *args): 84 | if event not in self._events: return self 85 | for callback in self._events[event]: 86 | callback(*args) 87 | return self 88 | 89 | def focus(self): 90 | sublime.set_timeout(lambda: sublime.active_window().focus_view(self.view), 0) 91 | 92 | def close(self, reason=None): 93 | if self.state != "closed": 94 | self.state = "closed" 95 | print("closed "+self.doc.name+(": "+reason if reason else '')) 96 | self.doc.close() 97 | SublimeListener.removeListener("modified", self._on_view_modified) 98 | SublimeListener.removeListener("close", self._on_view_close) 99 | SublimeListener.removeListener("post_save", self._on_view_post_save) 100 | self.doc.removeListener("closed", self.close) 101 | self.doc.removeListener("remoteop", self._on_doc_remoteop) 102 | self.view = None 103 | self.doc = None 104 | self.emit("closed") 105 | 106 | def _on_view_modified(self, view): 107 | if self.in_remoteop: return 108 | if self.view == None: return 109 | if view.id() == self.view.id() and self.doc: 110 | self._apply_change(self.doc, self.doc.get_text(), self._get_text()) 111 | 112 | def _on_view_post_save(self, view): 113 | if self.view == None: return 114 | if view.id() == self.view.id() and self.doc: 115 | view.set_scratch(False) 116 | 117 | def _on_view_close(self, view): 118 | if self.view == None: return 119 | if view.id() == self.view.id() and self.doc: 120 | self.close() 121 | 122 | def _apply_change(self, doc, oldval, newval): 123 | if oldval == newval: 124 | return 125 | 126 | commonStart = 0 127 | while commonStart < len(oldval) and commonStart < len(newval) and oldval[commonStart] == newval[commonStart]: 128 | commonStart+=1 129 | 130 | commonEnd = 0 131 | while commonEnd+commonStart < len(oldval) and commonEnd+commonStart < len(newval) and oldval[len(oldval)-1-commonEnd] == newval[len(newval)-1-commonEnd]: 132 | commonEnd+=1 133 | 134 | if len(oldval) != commonStart+commonEnd: 135 | doc.delete(commonStart, len(oldval)-commonStart-commonEnd) 136 | if len(newval) != commonStart+commonEnd: 137 | doc.insert(commonStart, newval[commonStart:len(newval)-commonEnd]) 138 | 139 | def _on_doc_remoteop(self, op, old_snapshot): 140 | sublime.set_timeout(lambda: self._apply_remoteop(op), 0) 141 | 142 | def _get_text(self): 143 | return self.view.substr(sublime.Region(0, self.view.size())).replace('\r\n', '\n') 144 | 145 | def _initialize(self, text): 146 | if self._get_text() == text: return 147 | edit = self.view.begin_edit() 148 | self.view.replace(edit, sublime.Region(0, self.view.size()), text) 149 | self.view.end_edit(edit) 150 | 151 | def _apply_remoteop(self, op): 152 | self.in_remoteop = True 153 | edit = self.view.begin_edit() 154 | for component in op: 155 | if 'i' in component: 156 | self.view.insert(edit, component['p'], component['i']) 157 | else: 158 | self.view.erase(edit, sublime.Region(component['p'], component['p']+len(component['d']))) 159 | self.view.end_edit(edit) 160 | self.in_remoteop = False 161 | 162 | 163 | 164 | client = None 165 | server = None 166 | editors = {} 167 | 168 | class SublimeCollaboration(object): 169 | def connect(self, host): 170 | global client 171 | if client: self.disconnect() 172 | client = collab.client.CollabClient(host, 6633) 173 | client.on('error', lambda error: sublime.error_message("Client error: {0}".format(error))) 174 | client.on('closed', self.on_close) 175 | print("connected") 176 | 177 | def disconnect(self): 178 | global client 179 | if not client: return 180 | client.disconnect() 181 | 182 | def on_close(self, reason=None): 183 | global client 184 | if not client: return 185 | client = None 186 | print("disconnected") 187 | 188 | def open_get_docs(self, error, items): 189 | global client 190 | if not client: return 191 | if error: 192 | sublime.error_message("Error retrieving document names: {0}".format(error)) 193 | else: 194 | if items: 195 | sublime.set_timeout(lambda: sublime.active_window().show_quick_panel(items, lambda x: None if x < 0 else self.open(items[x])), 0) 196 | else: 197 | sublime.error_message("No documents availible to open") 198 | 199 | def open(self, name): 200 | global client 201 | if not client: return 202 | if name in editors: 203 | return editors[name].focus() 204 | client.open(name, self.open_callback) 205 | 206 | def add_current(self, name): 207 | global client 208 | if not client: return 209 | if name in editors: 210 | return editors[name].focus() 211 | view = sublime.active_window().active_view() 212 | if view != None: 213 | client.open(name, lambda error, doc: self.add_callback(view, error, doc), snapshot=view.substr(sublime.Region(0, view.size()))) 214 | 215 | def open_callback(self, error, doc): 216 | if error: 217 | sublime.error_message("Error opening document: {0}".format(error)) 218 | else: 219 | sublime.set_timeout(lambda: self.create_editor(doc), 0) 220 | 221 | def add_callback(self, view, error, doc): 222 | if error: 223 | sublime.error_message("Error adding document: {0}".format(error)) 224 | else: 225 | sublime.set_timeout(lambda: self.add_editor(view, doc), 0) 226 | 227 | def create_editor(self, doc): 228 | view = sublime.active_window().new_file() 229 | view.set_scratch(True) 230 | view.set_name(doc.name) 231 | self.add_editor(view, doc) 232 | 233 | def add_editor(self, view, doc): 234 | global editors 235 | editor = SublimeEditor(view, doc) 236 | editor.on('closed', lambda: editors.pop(doc.name)) 237 | editors[doc.name] = editor 238 | 239 | def toggle_server(self): 240 | global server 241 | if server: 242 | server.close() 243 | server = None 244 | print("server closed") 245 | else: 246 | server = collab.server.CollabServer({'host':'127.0.0.1', 'port':6633}) 247 | server.run_forever() 248 | print("server started") 249 | 250 | class CollabConnectToServerCommand(sublime_plugin.ApplicationCommand, SublimeCollaboration): 251 | def run(self): 252 | sublime.active_window().show_input_panel("Enter server IP:", "localhost", self.connect, None, None) 253 | 254 | class CollabDisconnectFromServerCommand(sublime_plugin.ApplicationCommand, SublimeCollaboration): 255 | def run(self): 256 | self.disconnect() 257 | def is_enabled(self): 258 | global client 259 | return client 260 | 261 | class CollabToggleServerCommand(sublime_plugin.ApplicationCommand, SublimeCollaboration): 262 | def run(self): 263 | self.toggle_server() 264 | 265 | class CollabOpenDocumentCommand(sublime_plugin.ApplicationCommand, SublimeCollaboration): 266 | def run(self): 267 | global client 268 | if not client: return 269 | client.get_docs(self.open_get_docs) 270 | def is_enabled(self): 271 | global client 272 | return client 273 | 274 | class CollabAddCurrentDocumentCommand(sublime_plugin.ApplicationCommand, SublimeCollaboration): 275 | def run(self): 276 | global client 277 | if not client: return 278 | if sublime.active_window() == None: return 279 | if sublime.active_window().active_view() == None: return 280 | sublime.active_window().show_input_panel("Enter new document name:", self.current_file(), self.add_current, None, None) 281 | def is_enabled(self): 282 | global client 283 | return client 284 | def current_file(self): 285 | return os.path.split(sublime.active_window().active_view().file_name())[-1] 286 | --------------------------------------------------------------------------------