├── .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 | 
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 | [](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 |
--------------------------------------------------------------------------------