├── .gitignore ├── LICENSE ├── README.md ├── doc └── CoVim.txt └── plugin ├── CoVimClient.vim └── CoVimServer.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Fred Schott, Sam Haney 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the “Software”), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CoVim 2 | ========================== 3 | Collaborative Editing for Vim (One of Vim's [most requested features](http://www.vim.org/sponsor/vote_results.php)) is finally here! Think Google Docs for Vim. 4 | 5 | __By: Fred Schott, Sam Haney__ 6 | __Follow [@FredKSchott](http://www.twitter.com/fredkschott) for development news and updates!__ 7 | 8 | 9 | 10 | 11 | ![Demo Gif](http://i.imgur.com/CZeKkAI.gif "Demo Gif") 12 | 13 | ## Features 14 | - Allows multiple users to connect to the same document online 15 | - Displays collaborators with uniquely colored cursors 16 | - Works with your existing configuration 17 | - Easy to set up & use 18 | - And [More!](http://fredkschott.com/post/2013/05/introducing-covim-real-time-collaboration-for-vim/) 19 | 20 | ## Installation 21 | 22 | CoVim requires a version of Vim compiled with python 2.5+. Visit [Troubleshooting](https://github.com/FredKSchott/CoVim/wiki#troubleshooting) if you're having trouble starting Vim. 23 | Also note that the Twisted & Argparse libraries can also be installed via apt-get & yum. 24 | 25 | #### Install With [Pathogen](https://github.com/tpope/vim-pathogen): 26 | 27 | 1. `pip install twisted argparse service_identity` 28 | 2. `cd ~/.vim/bundle` 29 | 3. `git clone git://github.com/FredKSchott/CoVim.git` 30 | 31 | #### Install With [Vundle](https://github.com/gmarik/vundle): 32 | 33 | 1. `pip install twisted argparse service_identity` 34 | 2. Add `Plugin 'FredKSchott/CoVim'` to your `~/.vimrc` 35 | 3. `vim +PluginInstall +qall` 36 | 37 | #### Install Manually: 38 | 39 | 1. `pip install twisted argparse service_identity` 40 | 2. Add `CoVimClient.vim` & `CoVimServer.py` to `~/.vim/plugin/` 41 | 42 | > If Vim is having trouble finding modules (twisted, argparse, etc) do the following: 43 | > 44 | > 1. run `pip show MODULE_NAME` and get the `Location:` path 45 | > 2. add the following line to your .vimrc: `python import sys; sys.path.append("/module/location/path/")` using the module path found in step 1. 46 | > 3. Repeat until all modules are included in your path 47 | > 48 | > If you're still having trouble, [visit the wiki](https://github.com/FredKSchott/CoVim/wiki) for additional troubleshooting & FAQ 49 | 50 | ## Usage 51 | __To start a new CoVim server:__ `:CoVim start [port] [name]` (or, from the command line: `./CoVimServer.py [port]`) 52 | __To connect to a running server:__ `:CoVim connect [host address / 'localhost'] [port] [name]` 53 | __To disconnect:__ `:CoVim disconnect` 54 | __To quit Vim while CoVim is connected:__ `:CoVim quit` or `:qall!` 55 | 56 | 57 | ## Customization 58 | #### Add any the following to your .vimrc to customize CoVim: 59 | 60 | ``` 61 | let CoVim_default_name = "YOURNAME" 62 | let CoVim_default_port = "YOURPORT" 63 | ``` 64 | 65 | ## Links 66 | __[Announcement Post](http://www.fredkschott.com/post/50510962864/introducing-covim-collaborative-editing-for-vim)__ 67 | __[FAQ](https://github.com/FredKSchott/CoVim/wiki#faq)__ 68 | __[Troubleshooting](https://github.com/FredKSchott/CoVim/wiki#troubleshooting)__ 69 | 70 | 71 | ## Special Thanks 72 | Tufts Professor [Ming Chow](http://www.linkedin.com/in/mchow01) for leading the [Senior Capstone Project](http://tuftsdev.github.io/SoftwareEngineering/) that CoVim was born in. 73 | 74 | [![Analytics](https://ga-beacon.appspot.com/UA-39778226-2/CoVim/Readme.md)](https://github.com/igrigorik/ga-beacon) 75 | 76 | -------------------------------------------------------------------------------- /doc/CoVim.txt: -------------------------------------------------------------------------------- 1 | *CoVim.txt* Collaborative Editing for Vim 2 | *covim* 3 | 4 | 5 | ___ _ ~ 6 | _ / __\___/\ /(_)_ __ ___ ~ 7 | (_)/ / / _ \ \ / / | '_ ` _ \ ~ 8 | _/ /__| (_) \ V /| | | | | | | ~ 9 | (_)____/\___/ \_/ |_|_| |_| |_| ~ 10 | 11 | Reference Manual~ 12 | 13 | ============================================================================== 14 | CONTENTS *covim-contents* 15 | 16 | 1.Intro...................................|covim-intro| 17 | 2.Usage...................................|covim-usage| 18 | 3.Customization...........................|covim-customization| 19 | 4.About...................................|covim-about| 20 | 5.License.................................|covim-license| 21 | 22 | ============================================================================== 23 | INTRO *covim-intro* 24 | 25 | CoVim provides an easy way to add people to your Vim session for real-time 26 | collaboration. Once connected, other users will show up in your Vim client 27 | with their own cursors, able to move around and edit anywhere in the document. 28 | 29 | Demo: 30 | https://github.com/FredKSchott/CoVim 31 | 32 | 33 | ============================================================================== 34 | USAGE *covim-usage* 35 | 36 | To start a new CoVim server: 37 | :CoVim start [port] [name] 38 | (or, start a CoVim server from the command line: ./CoVimServer.py [port]) 39 | 40 | To connect to a running server: 41 | :CoVim connect [host address / 'localhost'] [port] [name] 42 | 43 | To disconnect: 44 | :CoVim disconnect 45 | (CoVim automatically disconnects when quitting Vim) 46 | 47 | To quit Vim while CoVim is connected: 48 | :CoVim quit 49 | (or :qall!) 50 | 51 | ============================================================================== 52 | CUSTOMIZATION *covim-customization* 53 | 54 | Setting Default Name & Port: 55 | Add these lines to your .vimrc: 56 | let CoVim_default_name = "YOURNAME" 57 | let CoVim_default_port = "YOURPORT" 58 | 59 | ============================================================================== 60 | ABOUT *covim-about* 61 | 62 | CoVim was created by: 63 | Fred K. Schott (github: fredkschott) 64 | Sam M. Haney 65 | 66 | CoVim is maintained on GitHub by: 67 | Fred K. Schott (github: fredkschott) 68 | 69 | Find the latest version of CoVim here: 70 | https://github.com/FredKSchott/CoVim 71 | 72 | 73 | ============================================================================== 74 | LICENCE *covim-licence* 75 | 76 | CoVim is licensed under MIT License. See LICENSE file for full license. 77 | 78 | 79 | vim:tw=78:sw=4:ft=help:norl: 80 | -------------------------------------------------------------------------------- /plugin/CoVimClient.vim: -------------------------------------------------------------------------------- 1 | "Check for Python Support" 2 | if !has('python') 3 | com! -nargs=* CoVim echoerr "Error: CoVim requires vim compiled with +python" 4 | finish 5 | endif 6 | 7 | com! -nargs=* CoVim py CoVim.command() 8 | 9 | "Needs to be set on connect, MacVim overrides otherwise" 10 | function! SetCoVimColors () 11 | hi CursorUser gui=bold term=bold cterm=bold 12 | hi Cursor1 ctermbg=DarkRed ctermfg=White guibg=DarkRed guifg=White gui=bold term=bold cterm=bold 13 | hi Cursor2 ctermbg=DarkBlue ctermfg=White guibg=DarkBlue guifg=White gui=bold term=bold cterm=bold 14 | hi Cursor3 ctermbg=DarkGreen ctermfg=White guibg=DarkGreen guifg=White gui=bold term=bold cterm=bold 15 | hi Cursor4 ctermbg=DarkCyan ctermfg=White guibg=DarkCyan guifg=White gui=bold term=bold cterm=bold 16 | hi Cursor5 ctermbg=DarkMagenta ctermfg=White guibg=DarkMagenta guifg=White gui=bold term=bold cterm=bold 17 | hi Cursor6 ctermbg=Brown ctermfg=White guibg=Brown guifg=White gui=bold term=bold cterm=bold 18 | hi Cursor7 ctermbg=LightRed ctermfg=Black guibg=LightRed guifg=Black gui=bold term=bold cterm=bold 19 | hi Cursor8 ctermbg=LightBlue ctermfg=Black guibg=LightBlue guifg=Black gui=bold term=bold cterm=bold 20 | hi Cursor9 ctermbg=LightGreen ctermfg=Black guibg=LightGreen guifg=Black gui=bold term=bold cterm=bold 21 | hi Cursor10 ctermbg=LightCyan ctermfg=Black guibg=LightCyan guifg=Black gui=bold term=bold cterm=bold 22 | hi Cursor0 ctermbg=LightYellow ctermfg=Black guibg=LightYellow guifg=Black gui=bold term=bold cterm=bold 23 | endfunction 24 | 25 | if !exists("CoVim_default_name") 26 | let CoVim_default_name = 0 27 | endif 28 | if !exists("CoVim_default_port") 29 | let CoVim_default_port = 0 30 | endif 31 | 32 | python << EOF 33 | 34 | import vim 35 | import os 36 | import json 37 | import warnings 38 | from twisted.internet.protocol import ClientFactory, Protocol 39 | from twisted.internet import reactor 40 | from threading import Thread 41 | from time import sleep 42 | 43 | # Ignore Warnings 44 | warnings.filterwarnings('ignore', '.*', UserWarning) 45 | warnings.filterwarnings('ignore', '.*', DeprecationWarning) 46 | 47 | # Find the server path 48 | CoVimServerPath = vim.eval('expand(":h")') + '/CoVimServer.py' 49 | 50 | ## CoVim Protocol 51 | class CoVimProtocol(Protocol): 52 | def __init__(self, fact): 53 | self.fact = fact 54 | 55 | def send(self, event): 56 | self.transport.write(event) 57 | 58 | def connectionMade(self): 59 | self.send(CoVim.username) 60 | 61 | def dataReceived(self, data_string): 62 | def to_utf8(d): 63 | if isinstance(d, dict): 64 | # no dict comprehension in python2.5/2.6 65 | d2 = {} 66 | for key, value in d.iteritems(): 67 | d2[to_utf8(key)] = to_utf8(value) 68 | return d2 69 | elif isinstance(d, list): 70 | return map(to_utf8, d) 71 | elif isinstance(d, unicode): 72 | return d.encode('utf-8') 73 | else: 74 | return d 75 | 76 | def clean_data_string(d_s): 77 | bad_data = d_s.find("}{") 78 | if bad_data > -1: 79 | d_s = d_s[:bad_data+1] 80 | return d_s 81 | 82 | data_string = clean_data_string(data_string) 83 | packet = to_utf8(json.loads(data_string)) 84 | if 'packet_type' in packet.keys(): 85 | data = packet['data'] 86 | if packet['packet_type'] == 'message': 87 | if data['message_type'] == 'error_newname_taken': 88 | CoVim.disconnect() 89 | print 'ERROR: Name already in use. Please try a different name' 90 | if data['message_type'] == 'error_newname_invalid': 91 | CoVim.disconnect() 92 | print 'ERROR: Name contains illegal characters. Only numbers, letters, underscores, and dashes allowed. Please try a different name' 93 | if data['message_type'] == 'connect_success': 94 | CoVim.setupWorkspace() 95 | if 'buffer' in data.keys(): 96 | CoVim.vim_buffer = data['buffer'] 97 | vim.current.buffer[:] = CoVim.vim_buffer 98 | CoVim.addUsers(data['collaborators']) 99 | print 'Success! You\'re now connected [Port '+str(CoVim.port)+']' 100 | if data['message_type'] == 'user_connected': 101 | CoVim.addUsers([data['user']]) 102 | print data['user']['name']+' connected to this document' 103 | if data['message_type'] == 'user_disconnected': 104 | CoVim.remUser(data['name']) 105 | print data['name']+' disconnected from this document' 106 | if packet['packet_type'] == 'update': 107 | if 'buffer' in data.keys() and data['name'] != CoVim.username: 108 | b_data = data['buffer'] 109 | CoVim.vim_buffer = vim.current.buffer[:b_data['start']] \ 110 | + b_data['buffer'] \ 111 | + vim.current.buffer[b_data['end']-b_data['change_y']+1:] 112 | vim.current.buffer[:] = CoVim.vim_buffer 113 | if 'updated_cursors' in data.keys(): 114 | # We need to update your own cursor as soon as possible, then update other cursors after 115 | for updated_user in data['updated_cursors']: 116 | if CoVim.username == updated_user['name'] and data['name'] != CoVim.username: 117 | vim.current.window.cursor = (updated_user['cursor']['y'], updated_user['cursor']['x']) 118 | for updated_user in data['updated_cursors']: 119 | if CoVim.username != updated_user['name']: 120 | vim.command(':call matchdelete('+str(CoVim.collab_manager.collaborators[updated_user['name']][1]) + ')') 121 | vim.command(':call matchadd(\''+CoVim.collab_manager.collaborators[updated_user['name']][0]+'\', \'\%' + str(updated_user['cursor']['x']) + 'v.\%'+str(updated_user['cursor']['y'])+'l\', 10, ' + str(CoVim.collab_manager.collaborators[updated_user['name']][1]) + ')') 122 | #data['cursor']['x'] = max(1,data['cursor']['x']) 123 | #print str(data['cursor']['x'])+', '+str(data['cursor']['y']) 124 | vim.command(':redraw') 125 | 126 | 127 | #CoVimFactory - Handles Socket Communication 128 | class CoVimFactory(ClientFactory): 129 | 130 | def buildProtocol(self, addr): 131 | self.p = CoVimProtocol(self) 132 | return self.p 133 | 134 | def startFactory(self): 135 | self.isConnected = True 136 | 137 | def stopFactory(self): 138 | self.isConnected = False 139 | 140 | def buff_update(self): 141 | d = { 142 | "packet_type": "update", 143 | "data": { 144 | "cursor": { 145 | "x": max(1, vim.current.window.cursor[1]), 146 | "y": vim.current.window.cursor[0] 147 | }, 148 | "name": CoVim.username 149 | } 150 | } 151 | d = self.create_update_packet(d) 152 | data = json.dumps(d) 153 | self.p.send(data) 154 | 155 | def cursor_update(self): 156 | d = { 157 | "packet_type": "update", 158 | "data": { 159 | "cursor": { 160 | "x": max(1, vim.current.window.cursor[1]+1), 161 | "y": vim.current.window.cursor[0] 162 | }, 163 | "name": CoVim.username 164 | } 165 | } 166 | d = self.create_update_packet(d) 167 | data = json.dumps(d) 168 | self.p.send(data) 169 | 170 | def create_update_packet(self, d): 171 | current_buffer = vim.current.buffer[:] 172 | if current_buffer != CoVim.vim_buffer: 173 | cursor_y = vim.current.window.cursor[0] - 1 174 | change_y = len(current_buffer) - len(CoVim.vim_buffer) 175 | change_x = 0 176 | if len(CoVim.vim_buffer) > cursor_y-change_y and cursor_y-change_y >= 0 \ 177 | and len(current_buffer) > cursor_y and cursor_y >= 0: 178 | change_x = len(current_buffer[cursor_y]) - len(CoVim.vim_buffer[cursor_y-change_y]) 179 | limits = { 180 | 'from': max(0, cursor_y-abs(change_y)), 181 | 'to': min(len(vim.current.buffer)-1, cursor_y+abs(change_y)) 182 | } 183 | d_buffer = { 184 | 'start': limits['from'], 185 | 'end': limits['to'], 186 | 'change_y': change_y, 187 | 'change_x': change_x, 188 | 'buffer': vim.current.buffer[limits['from']:limits['to']+1], 189 | 'buffer_size': len(current_buffer) 190 | } 191 | d['data']['buffer'] = d_buffer 192 | CoVim.vim_buffer = current_buffer 193 | return d 194 | 195 | def clientConnectionLost(self, connector, reason): 196 | #THIS IS A HACK 197 | if hasattr(CoVim, 'buddylist'): 198 | CoVim.disconnect() 199 | print 'Lost connection.' 200 | 201 | def clientConnectionFailed(self, connector, reason): 202 | CoVim.disconnect() 203 | print 'Connection failed.' 204 | 205 | 206 | #Manage Collaborators 207 | class CollaboratorManager: 208 | 209 | def __init__(self): 210 | self.collab_id_itr = 4 211 | self.reset() 212 | 213 | def reset(self): 214 | self.collab_color_itr = 1 215 | self.collaborators = {} 216 | self.buddylist_highlight_ids = [] 217 | 218 | def addUser(self, user_obj): 219 | if user_obj['name'] == CoVim.username: 220 | self.collaborators[user_obj['name']] = ('CursorUser', 4000) 221 | else: 222 | self.collaborators[user_obj['name']] = ('Cursor' + str(self.collab_color_itr), self.collab_id_itr) 223 | self.collab_id_itr += 1 224 | self.collab_color_itr = (self.collab_id_itr-3) % 11 225 | vim.command(':call matchadd(\''+self.collaborators[user_obj['name']][0]+'\', \'\%' + str(user_obj['cursor']['x']) + 'v.\%'+str(user_obj['cursor']['y'])+'l\', 10, ' + str(self.collaborators[user_obj['name']][1]) + ')') 226 | self.refreshCollabDisplay() 227 | 228 | def remUser(self, name): 229 | vim.command('call matchdelete('+str(self.collaborators[name][1]) + ')') 230 | del(self.collaborators[name]) 231 | self.refreshCollabDisplay() 232 | 233 | def refreshCollabDisplay(self): 234 | buddylist_window_width = int(vim.eval('winwidth(0)')) 235 | CoVim.buddylist[:] = [''] 236 | x_a = 1 237 | line_i = 0 238 | vim.command("1wincmd w") 239 | for match_id in self.buddylist_highlight_ids: 240 | vim.command('call matchdelete('+str(match_id) + ')') 241 | self.buddylist_highlight_ids = [] 242 | for name in self.collaborators.keys(): 243 | x_b = x_a + len(name) 244 | if x_b > buddylist_window_width: 245 | line_i += 1 246 | x_a = 1 247 | x_b = x_a + len(name) 248 | CoVim.buddylist.append('') 249 | vim.command('resize '+str(line_i+1)) 250 | CoVim.buddylist[line_i] += name+' ' 251 | self.buddylist_highlight_ids.append(vim.eval('matchadd(\''+self.collaborators[name][0]+'\',\'\%<'+str(x_b)+'v.\%>'+str(x_a)+'v\%'+str(line_i+1)+'l\',10,'+str(self.collaborators[name][1]+2000)+')')) 252 | x_a = x_b + 1 253 | vim.command("wincmd p") 254 | 255 | 256 | #Manage all of CoVim 257 | class CoVimScope: 258 | 259 | def initiate(self, addr, port, name): 260 | #Check if connected. If connected, throw error. 261 | if hasattr(self, 'fact') and self.fact.isConnected: 262 | print 'ERROR: Already connected. Please disconnect first' 263 | return 264 | if not port and hasattr(self, 'port') and self.port: 265 | port = self.port 266 | if not addr and hasattr(self, 'addr') and self.addr: 267 | addr = self.addr 268 | if not addr or not port or not name: 269 | print 'Syntax Error: Use form :Covim connect ' 270 | return 271 | port = int(port) 272 | addr = str(addr) 273 | vim.command('autocmd VimLeave * py CoVim.quit()') 274 | if not hasattr(self, 'connection'): 275 | self.addr = addr 276 | self.port = port 277 | self.username = name 278 | self.vim_buffer = [] 279 | self.fact = CoVimFactory() 280 | self.collab_manager = CollaboratorManager() 281 | self.connection = reactor.connectTCP(addr, port, self.fact) 282 | self.reactor_thread = Thread(target=reactor.run, args=(False,)) 283 | self.reactor_thread.start() 284 | print 'Connecting...' 285 | elif (hasattr(self, 'port') and port != self.port) or (hasattr(self, 'addr') and addr != self.addr): 286 | print 'ERROR: Different address/port already used. To try another, you need to restart Vim' 287 | else: 288 | self.collab_manager.reset() 289 | self.connection.connect() 290 | print 'Reconnecting...' 291 | 292 | def createServer(self, port, name): 293 | vim.command(':silent execute "!'+CoVimServerPath+' '+port+' &>/dev/null &"') 294 | sleep(0.5) 295 | self.initiate('localhost', port, name) 296 | 297 | def setupWorkspace(self): 298 | vim.command('call SetCoVimColors()') 299 | vim.command(':autocmd!') 300 | vim.command('autocmd CursorMoved py reactor.callFromThread(CoVim.fact.cursor_update)') 301 | vim.command('autocmd CursorMovedI py reactor.callFromThread(CoVim.fact.buff_update)') 302 | vim.command('autocmd VimLeave * py CoVim.quit()') 303 | vim.command("1new +setlocal\ stl=%!'CoVim-Collaborators'") 304 | self.buddylist = vim.current.buffer 305 | self.buddylist_window = vim.current.window 306 | vim.command("wincmd j") 307 | 308 | def addUsers(self, list): 309 | map(self.collab_manager.addUser, list) 310 | 311 | def remUser(self, name): 312 | self.collab_manager.remUser(name) 313 | 314 | def refreshCollabDisplay(self): 315 | self.collab_manager.refreshCollabDisplay() 316 | 317 | def command(self, arg1=False, arg2=False, arg3=False, arg4=False): 318 | default_name = vim.eval('CoVim_default_name') 319 | default_name_string = " - default: '"+default_name+"'" if default_name != '0' else "" 320 | default_port = vim.eval('CoVim_default_port') 321 | default_port_string = " - default: "+default_port if default_port != '0' else "" 322 | if arg1 == "connect": 323 | if arg2 and arg3 and arg4: 324 | self.initiate(arg2, arg3, arg4) 325 | elif arg2 and arg3 and default_name != '0': 326 | self.initiate(arg2, arg3, default_name) 327 | elif arg2 and default_port != '0' and default_name != '0': 328 | self.initiate(arg2, default_port, default_name) 329 | else: 330 | print "usage :CoVim connect [host address / 'localhost'] [port"+default_port_string+"] [name"+default_name_string+"]" 331 | elif arg1 == "disconnect": 332 | self.disconnect() 333 | elif arg1 == "quit": 334 | self.exit() 335 | elif arg1 == "start": 336 | if arg2 and arg3: 337 | self.createServer(arg2, arg3) 338 | elif arg2 and default_name != '0': 339 | self.createServer(arg2, default_name) 340 | elif default_port != '0' and default_name != '0': 341 | self.createServer(default_port, default_name) 342 | else: 343 | print "usage :CoVim start [port"+default_port_string+"] [name"+default_name_string+"]" 344 | else: 345 | print "usage: CoVim [start] [connect] [disconnect] [quit]" 346 | 347 | def exit(self): 348 | if hasattr(self, 'buddylist_window') and hasattr(self, 'connection'): 349 | self.disconnect() 350 | vim.command('q') 351 | else: 352 | print "ERROR: CoVim must be running to use this command" 353 | 354 | def disconnect(self): 355 | if hasattr(self, 'buddylist'): 356 | vim.command("1wincmd w") 357 | vim.command("q!") 358 | self.collab_manager.buddylist_highlight_ids = [] 359 | for name in self.collab_manager.collaborators.keys(): 360 | if name != CoVim.username: 361 | vim.command(':call matchdelete('+str(self.collab_manager.collaborators[name][1]) + ')') 362 | del(self.buddylist) 363 | if hasattr(self, 'buddylist_window'): 364 | del(self.buddylist_window) 365 | if hasattr(self, 'connection'): 366 | reactor.callFromThread(self.connection.disconnect) 367 | print 'Successfully disconnected from document!' 368 | else: 369 | print "ERROR: CoVim must be running to use this command" 370 | 371 | def quit(self): 372 | reactor.callFromThread(reactor.stop) 373 | 374 | CoVim = CoVimScope() 375 | 376 | EOF 377 | -------------------------------------------------------------------------------- /plugin/CoVimServer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | import re 4 | import json 5 | import argparse 6 | 7 | from twisted.internet.protocol import Factory, Protocol 8 | #from twisted.protocols.basic import LineReceiver 9 | from twisted.internet import reactor 10 | 11 | parser = argparse.ArgumentParser(description='Start a CoVim server.') 12 | parser.add_argument('-p', '--persist', action='store_true', 13 | help='Keep server running if all users disconnect') 14 | parser.add_argument('port', type=int, nargs='?', default=8555, 15 | help='Port number to run on') 16 | 17 | 18 | def name_validate(strg, search=re.compile(r'[^0-9a-zA-Z\-\_]').search): 19 | return not bool(search(strg)) 20 | 21 | 22 | class React(Protocol): 23 | 24 | def __init__(self, factory): 25 | self.factory = factory 26 | self.state = "GETNAME" 27 | 28 | def dataReceived(self, data): 29 | if self.state == "GETNAME": 30 | self.handle_GETNAME(data) 31 | else: 32 | self.handle_BUFF(data) 33 | 34 | def handle_GETNAME(self, name): 35 | # Handle duplicate name 36 | if userManager.has_user(name): 37 | d = { 38 | 'packet_type': 'message', 39 | 'data': { 40 | 'message_type': 'error_newname_taken' 41 | } 42 | } 43 | self.transport.write(json.dumps(d)) 44 | return 45 | 46 | # Handle spaces in name 47 | if not name_validate(name): 48 | d = { 49 | 'packet_type': 'message', 50 | 'data': { 51 | 'message_type': 'error_newname_invalid' 52 | } 53 | } 54 | self.transport.write(json.dumps(d)) 55 | return 56 | 57 | # Name is Valid, Add to Document 58 | self.user = User(name, self) 59 | userManager.add_user(self.user) 60 | self.state = "CHAT" 61 | d = { 62 | 'packet_type': 'message', 63 | 'data': { 64 | 'message_type': 'connect_success', 65 | 'name': name, 66 | 'collaborators': userManager.all_users_to_json() 67 | } 68 | } 69 | 70 | if userManager.is_multi(): 71 | d['data']['buffer'] = self.factory.buff 72 | self.transport.write(json.dumps(d)) 73 | print 'User "{user_name}" Connected'.format(user_name=self.user.name) 74 | 75 | # Alert other Collaborators of new user 76 | d = { 77 | 'packet_type': 'message', 78 | 'data': { 79 | 'message_type': 'user_connected', 80 | 'user': self.user.to_json() 81 | } 82 | } 83 | self.user.broadcast_packet(d) 84 | 85 | def handle_BUFF(self, data_string): 86 | def to_utf8(d): 87 | if isinstance(d, dict): 88 | # no dict comprehension in python2.5/2.6 89 | d2 = {} 90 | for key, value in d.iteritems(): 91 | d2[to_utf8(key)] = to_utf8(value) 92 | return d2 93 | elif isinstance(d, list): 94 | return map(to_utf8, d) 95 | elif isinstance(d, unicode): 96 | return d.encode('utf-8') 97 | else: 98 | return d 99 | 100 | def clean_data_string(d_s): 101 | bad_data = d_s.find("}{") 102 | if bad_data > -1: 103 | d_s = d_s[:bad_data+1] 104 | return d_s 105 | 106 | data_string = clean_data_string(data_string) 107 | d = to_utf8(json.loads(data_string)) 108 | data = d['data'] 109 | update_self = False 110 | 111 | if 'cursor' in data.keys(): 112 | user = userManager.get_user(data['name']) 113 | user.update_cursor(data['cursor']['x'], data['cursor']['y']) 114 | d['data']['updated_cursors'] = [user.to_json()] 115 | del d['data']['cursor'] 116 | 117 | if 'buffer' in data.keys(): 118 | b_data = data['buffer'] 119 | #TODO: Improve Speed: If change_y = 0, just replace that one line 120 | #print ' \\n '.join(self.factory.buff[:b_data['start']]) 121 | #print ' \\n '.join(b_data['buffer']) 122 | #print ' \\n '.join(self.factory.buff[b_data['end']-b_data['change_y']+1:]) 123 | self.factory.buff = self.factory.buff[:b_data['start']] \ 124 | + b_data['buffer'] \ 125 | + self.factory.buff[b_data['end']-b_data['change_y']+1:] 126 | d['data']['updated_cursors'] += userManager.update_cursors(b_data, user) 127 | update_self = True 128 | self.user.broadcast_packet(d, update_self) 129 | 130 | def connectionLost(self, reason): 131 | if hasattr(self, 'user'): 132 | userManager.rem_user(self.user) 133 | if userManager.is_empty(): 134 | print 'All users disconnected. Shutting down...' 135 | reactor.stop() 136 | 137 | 138 | class ReactFactory(Factory): 139 | 140 | def __init__(self): 141 | self.buff = [] 142 | 143 | def initiate(self, port): 144 | self.port = port 145 | print 'Now listening on port {port}...'.format(port=port) 146 | reactor.listenTCP(port, self) 147 | reactor.run() 148 | 149 | def buildProtocol(self, addr): 150 | return React(self) 151 | 152 | 153 | class Cursor: 154 | def __init__(self): 155 | self.x = 1 156 | self.y = 1 157 | 158 | def to_json(self): 159 | return { 160 | 'x': self.x, 161 | 'y': self.y 162 | } 163 | 164 | 165 | class User: 166 | def __init__(self, name, protocol): 167 | self.name = name 168 | self.protocol = protocol 169 | self.cursor = Cursor() 170 | 171 | def to_json(self): 172 | return { 173 | 'name': self.name, 174 | 'cursor': self.cursor.to_json() 175 | } 176 | 177 | def broadcast_packet(self, obj, send_to_self=False): 178 | obj_json = json.dumps(obj) 179 | #print obj_json 180 | for name, user in userManager.users.iteritems(): 181 | if user.name != self.name or send_to_self: 182 | user.protocol.transport.write(obj_json) 183 | #TODO: don't send yourself your own buffer, but del on a copy doesn't work 184 | 185 | def update_cursor(self, x, y): 186 | self.cursor.x = x 187 | self.cursor.y = y 188 | 189 | 190 | class UserManager: 191 | 192 | def __init__(self): 193 | self.users = {} 194 | 195 | def is_empty(self): 196 | return not self.users 197 | 198 | def is_multi(self): 199 | return len(self.users) > 1 200 | 201 | def has_user(self, search_name): 202 | return self.users.get(search_name) 203 | 204 | def add_user(self, u): 205 | self.users[u.name] = u 206 | 207 | def get_user(self, u_name): 208 | try: 209 | return self.users[u_name] 210 | except KeyError: 211 | raise Exception('user doesnt exist') 212 | 213 | def rem_user(self, user): 214 | if self.users.get(user.name): 215 | d = { 216 | 'packet_type': 'message', 217 | 'data': { 218 | 'message_type': 'user_disconnected', 219 | 'name': user.name 220 | } 221 | } 222 | user.broadcast_packet(d) 223 | print 'User "{user_name}" Disconnected'.format(user_name=user.name) 224 | del self.users[user.name] 225 | 226 | def all_users_to_json(self): 227 | return [user.to_json() for user in userManager.users.values()] 228 | 229 | def update_cursors(self, buffer_data, u): 230 | return_arr = [] 231 | y_target = u.cursor.y 232 | x_target = u.cursor.x 233 | 234 | for user in userManager.users.values(): 235 | updated = False 236 | if user != u: 237 | if user.cursor.y > y_target: 238 | user.cursor.y += buffer_data['change_y'] 239 | updated = True 240 | if user.cursor.y == y_target and user.cursor.x > x_target: 241 | user.cursor.x = max(1, user.cursor.x + buffer_data['change_x']) 242 | updated = True 243 | if user.cursor.y == y_target - 1 and user.cursor.x > x_target \ 244 | and buffer_data['change_y'] == 1: 245 | user.cursor.y += 1 246 | user.cursor.x = max(1, user.cursor.x + buffer_data['change_x']) 247 | updated = True 248 | #TODO: If the line was just split? 249 | if updated: 250 | return_arr.append(user.to_json()) 251 | return return_arr 252 | 253 | 254 | userManager = UserManager() 255 | 256 | if __name__ == '__main__': 257 | args = parser.parse_args() 258 | Server = ReactFactory() 259 | Server.initiate(args.port) 260 | --------------------------------------------------------------------------------