827
|
1 "pythoncomplete.vim - Omni Completion for python
|
838
|
2 " Maintainer: Aaron Griffin <aaronmgriffin@gmail.com>
|
|
3 " Version: 0.5
|
|
4 " Last Updated: 19 April 2006
|
827
|
5 "
|
838
|
6 " Yeah, I skipped a version number - 0.4 was never public.
|
|
7 " It was a bugfix version on top of 0.3. This is a complete
|
|
8 " rewrite.
|
827
|
9 "
|
838
|
10 " TODO:
|
|
11 " User defined docstrings aren't handled right...
|
|
12 " 'info' item output can use some formatting work
|
|
13 " Add an "unsafe eval" mode, to allow for return type evaluation
|
827
|
14
|
|
15 if !has('python')
|
|
16 echo "Error: Required vim compiled with +python"
|
|
17 finish
|
|
18 endif
|
|
19
|
|
20 function! pythoncomplete#Complete(findstart, base)
|
|
21 "findstart = 1 when we need to get the text length
|
838
|
22 if a:findstart == 1
|
827
|
23 let line = getline('.')
|
|
24 let idx = col('.')
|
|
25 while idx > 0
|
|
26 let idx -= 1
|
838
|
27 let c = line[idx]
|
827
|
28 if c =~ '\w'
|
|
29 continue
|
|
30 elseif ! c =~ '\.'
|
|
31 idx = -1
|
|
32 break
|
|
33 else
|
|
34 break
|
|
35 endif
|
|
36 endwhile
|
|
37
|
|
38 return idx
|
|
39 "findstart = 0 when we need to return the list of completions
|
|
40 else
|
838
|
41 "vim no longer moves the cursor upon completion... fix that
|
|
42 let line = getline('.')
|
|
43 let idx = col('.')
|
|
44 let cword = ''
|
|
45 while idx > 0
|
|
46 let idx -= 1
|
|
47 let c = line[idx]
|
|
48 if c =~ '\w' || c =~ '\.'
|
|
49 let cword = c . cword
|
|
50 continue
|
|
51 elseif strlen(cword) > 0 || idx == 0
|
|
52 break
|
|
53 endif
|
|
54 endwhile
|
|
55 execute "python vimcomplete('" . cword . "', '" . a:base . "')"
|
827
|
56 return g:pythoncomplete_completions
|
|
57 endif
|
|
58 endfunction
|
|
59
|
|
60 function! s:DefPython()
|
|
61 python << PYTHONEOF
|
838
|
62 import sys, tokenize, cStringIO, types
|
|
63 from token import NAME, DEDENT, NEWLINE, STRING
|
827
|
64
|
838
|
65 debugstmts=[]
|
|
66 def dbg(s): debugstmts.append(s)
|
|
67 def showdbg():
|
|
68 for d in debugstmts: print "DBG: %s " % d
|
827
|
69
|
838
|
70 def vimcomplete(context,match):
|
|
71 global debugstmts
|
|
72 debugstmts = []
|
827
|
73 try:
|
838
|
74 import vim
|
|
75 def complsort(x,y):
|
|
76 return x['abbr'] > y['abbr']
|
|
77 cmpl = Completer()
|
|
78 cmpl.evalsource('\n'.join(vim.current.buffer),vim.eval("line('.')"))
|
|
79 all = cmpl.get_completions(context,match)
|
|
80 all.sort(complsort)
|
|
81 dictstr = '['
|
|
82 # have to do this for double quoting
|
|
83 for cmpl in all:
|
|
84 dictstr += '{'
|
|
85 for x in cmpl: dictstr += '"%s":"%s",' % (x,cmpl[x])
|
|
86 dictstr += '"icase":0},'
|
|
87 if dictstr[-1] == ',': dictstr = dictstr[:-1]
|
|
88 dictstr += ']'
|
|
89 dbg("dict: %s" % dictstr)
|
|
90 vim.command("silent let g:pythoncomplete_completions = %s" % dictstr)
|
|
91 #dbg("Completion dict:\n%s" % all)
|
|
92 except vim.error:
|
|
93 dbg("VIM Error: %s" % vim.error)
|
|
94
|
|
95 class Completer(object):
|
|
96 def __init__(self):
|
|
97 self.compldict = {}
|
|
98 self.parser = PyParser()
|
|
99
|
|
100 def evalsource(self,text,line=0):
|
|
101 sc = self.parser.parse(text,line)
|
|
102 src = sc.get_code()
|
|
103 dbg("source: %s" % src)
|
|
104 try: exec(src) in self.compldict
|
|
105 except: dbg("parser: %s, %s" % (sys.exc_info()[0],sys.exc_info()[1]))
|
|
106 for l in sc.locals:
|
|
107 try: exec(l) in self.compldict
|
|
108 except: dbg("locals: %s, %s [%s]" % (sys.exc_info()[0],sys.exc_info()[1],l))
|
|
109
|
|
110 def _cleanstr(self,doc):
|
|
111 return doc.replace('"',' ')\
|
|
112 .replace("'",' ')\
|
|
113 .replace('\n',' ')\
|
|
114 .replace('\r',' ')\
|
|
115 .replace('
',' ')
|
|
116
|
|
117 def get_arguments(self,func_obj):
|
|
118 def _ctor(obj):
|
|
119 try: return class_ob.__init__.im_func
|
|
120 except AttributeError:
|
|
121 for base in class_ob.__bases__:
|
|
122 rc = _find_constructor(base)
|
|
123 if rc is not None: return rc
|
|
124 return None
|
827
|
125
|
838
|
126 arg_offset = 1
|
|
127 if type(func_obj) == types.ClassType: func_obj = _ctor(func_obj)
|
|
128 elif type(func_obj) == types.MethodType: func_obj = func_obj.im_func
|
|
129 else: arg_offset = 0
|
|
130
|
|
131 arg_text = ')'
|
|
132 if type(func_obj) in [types.FunctionType, types.LambdaType]:
|
|
133 try:
|
|
134 cd = func_obj.func_code
|
|
135 real_args = cd.co_varnames[arg_offset:cd.co_argcount]
|
|
136 defaults = func_obj.func_defaults or []
|
|
137 defaults = [map(lambda name: "=%s" % name, defaults)]
|
|
138 defaults = [""] * (len(real_args)-len(defaults)) + defaults
|
|
139 items = map(lambda a,d: a+d, real_args, defaults)
|
|
140 if func_obj.func_code.co_flags & 0x4:
|
|
141 items.append("...")
|
|
142 if func_obj.func_code.co_flags & 0x8:
|
|
143 items.append("***")
|
|
144 arg_text = ", ".join(items) + ')'
|
|
145
|
|
146 except:
|
|
147 dbg("completion: %s: %s" % (sys.exc_info()[0],sys.exc_info()[1]))
|
|
148 pass
|
|
149 if len(arg_text) == 0:
|
|
150 # The doc string sometimes contains the function signature
|
|
151 # this works for alot of C modules that are part of the
|
|
152 # standard library
|
|
153 doc = func_obj.__doc__
|
|
154 if doc:
|
|
155 doc = doc.lstrip()
|
|
156 pos = doc.find('\n')
|
|
157 if pos > 0:
|
|
158 sigline = doc[:pos]
|
|
159 lidx = sigline.find('(')
|
|
160 ridx = sigline.find(')')
|
|
161 if lidx > 0 and ridx > 0:
|
|
162 arg_text = sigline[lidx+1:ridx] + ')'
|
|
163 return arg_text
|
|
164
|
|
165 def get_completions(self,context,match):
|
|
166 dbg("get_completions('%s','%s')" % (context,match))
|
|
167 stmt = ''
|
|
168 if context: stmt += str(context)
|
|
169 if match: stmt += str(match)
|
|
170 try:
|
|
171 result = None
|
|
172 all = {}
|
|
173 ridx = stmt.rfind('.')
|
|
174 if len(stmt) > 0 and stmt[-1] == '(':
|
|
175 #TODO
|
|
176 result = eval(_sanitize(stmt[:-1]), self.compldict)
|
|
177 doc = result.__doc__
|
|
178 if doc == None: doc = ''
|
|
179 args = self.get_arguments(res)
|
|
180 return [{'word':self._cleanstr(args),'info':self._cleanstr(doc),'kind':'p'}]
|
|
181 elif ridx == -1:
|
|
182 match = stmt
|
|
183 all = self.compldict
|
|
184 else:
|
|
185 match = stmt[ridx+1:]
|
|
186 stmt = _sanitize(stmt[:ridx])
|
|
187 result = eval(stmt, self.compldict)
|
|
188 all = dir(result)
|
827
|
189
|
838
|
190 dbg("completing: stmt:%s" % stmt)
|
|
191 completions = []
|
|
192
|
|
193 try: maindoc = result.__doc__
|
|
194 except: maindoc = ' '
|
|
195 if maindoc == None: maindoc = ' '
|
|
196 for m in all:
|
|
197 if m == "_PyCmplNoType": continue #this is internal
|
|
198 try:
|
|
199 dbg('possible completion: %s' % m)
|
|
200 if m.find(match) == 0:
|
|
201 if result == None: inst = all[m]
|
|
202 else: inst = getattr(result,m)
|
|
203 try: doc = inst.__doc__
|
|
204 except: doc = maindoc
|
|
205 typestr = str(inst)
|
|
206 if doc == None or doc == '': doc = maindoc
|
|
207
|
|
208 wrd = m[len(match):]
|
|
209 c = {'word':wrd, 'abbr':m, 'info':self._cleanstr(doc),'kind':'m'}
|
|
210 if "function" in typestr:
|
|
211 c['word'] += '('
|
|
212 c['abbr'] += '(' + self._cleanstr(self.get_arguments(inst))
|
|
213 c['kind'] = 'f'
|
|
214 elif "method" in typestr:
|
|
215 c['word'] += '('
|
|
216 c['abbr'] += '(' + self._cleanstr(self.get_arguments(inst))
|
|
217 c['kind'] = 'f'
|
|
218 elif "module" in typestr:
|
|
219 c['word'] += '.'
|
|
220 c['kind'] = 'm'
|
|
221 elif "class" in typestr:
|
|
222 c['word'] += '('
|
|
223 c['abbr'] += '('
|
|
224 c['kind']='c'
|
|
225 completions.append(c)
|
|
226 except:
|
|
227 i = sys.exc_info()
|
|
228 dbg("inner completion: %s,%s [stmt='%s']" % (i[0],i[1],stmt))
|
|
229 return completions
|
|
230 except:
|
|
231 i = sys.exc_info()
|
|
232 dbg("completion: %s,%s [stmt='%s']" % (i[0],i[1],stmt))
|
|
233 return []
|
|
234
|
|
235 class Scope(object):
|
|
236 def __init__(self,name,indent):
|
|
237 self.subscopes = []
|
|
238 self.docstr = ''
|
|
239 self.locals = []
|
|
240 self.parent = None
|
|
241 self.name = name
|
|
242 self.indent = indent
|
|
243
|
|
244 def add(self,sub):
|
|
245 #print 'push scope: [%s@%s]' % (sub.name,sub.indent)
|
|
246 sub.parent = self
|
|
247 self.subscopes.append(sub)
|
|
248 return sub
|
827
|
249
|
838
|
250 def doc(self,str):
|
|
251 """ Clean up a docstring """
|
|
252 d = str.replace('\n',' ')
|
|
253 d = d.replace('\t',' ')
|
|
254 while d.find(' ') > -1: d = d.replace(' ',' ')
|
|
255 while d[0] in '"\'\t ': d = d[1:]
|
|
256 while d[-1] in '"\'\t ': d = d[:-1]
|
|
257 self.docstr = d
|
|
258
|
|
259 def local(self,loc):
|
|
260 if not self._hasvaralready(loc):
|
|
261 self.locals.append(loc)
|
|
262
|
|
263 def copy_decl(self,indent=0):
|
|
264 """ Copy a scope's declaration only, at the specified indent level - not local variables """
|
|
265 return Scope(self.name,indent)
|
|
266
|
|
267 def _hasvaralready(self,test):
|
|
268 "Convienance function... keep out duplicates"
|
|
269 if test.find('=') > -1:
|
|
270 var = test.split('=')[0].strip()
|
|
271 for l in self.locals:
|
|
272 if l.find('=') > -1 and var == l.split('=')[0].strip():
|
|
273 return True
|
|
274 return False
|
827
|
275
|
838
|
276 def get_code(self):
|
|
277 # we need to start with this, to fix up broken completions
|
|
278 # hopefully this name is unique enough...
|
|
279 str = '"""'+self.docstr+'"""\n'
|
|
280 str += 'class _PyCmplNoType:\n def __getattr__(self,name):\n return None\n'
|
|
281 for sub in self.subscopes:
|
|
282 str += sub.get_code()
|
|
283 #str += '\n'.join(self.locals)+'\n'
|
|
284
|
|
285 return str
|
|
286
|
|
287 def pop(self,indent):
|
|
288 #print 'pop scope: [%s] to [%s]' % (self.indent,indent)
|
|
289 outer = self
|
|
290 while outer.parent != None and outer.indent >= indent:
|
|
291 outer = outer.parent
|
|
292 return outer
|
|
293
|
|
294 def currentindent(self):
|
|
295 #print 'parse current indent: %s' % self.indent
|
|
296 return ' '*self.indent
|
|
297
|
|
298 def childindent(self):
|
|
299 #print 'parse child indent: [%s]' % (self.indent+1)
|
|
300 return ' '*(self.indent+1)
|
|
301
|
|
302 class Class(Scope):
|
|
303 def __init__(self, name, supers, indent):
|
|
304 Scope.__init__(self,name,indent)
|
|
305 self.supers = supers
|
|
306 def copy_decl(self,indent=0):
|
|
307 c = Class(self.name,self.supers,indent)
|
|
308 for s in self.subscopes:
|
|
309 c.add(s.copy_decl(indent+1))
|
|
310 return c
|
|
311 def get_code(self):
|
|
312 str = '%sclass %s' % (self.currentindent(),self.name)
|
|
313 if len(self.supers) > 0: str += '(%s)' % ','.join(self.supers)
|
|
314 str += ':\n'
|
|
315 if len(self.docstr) > 0: str += self.childindent()+'"""'+self.docstr+'"""\n'
|
|
316 if len(self.subscopes) > 0:
|
|
317 for s in self.subscopes: str += s.get_code()
|
827
|
318 else:
|
838
|
319 str += '%spass\n' % self.childindent()
|
|
320 return str
|
|
321
|
|
322
|
|
323 class Function(Scope):
|
|
324 def __init__(self, name, params, indent):
|
|
325 Scope.__init__(self,name,indent)
|
|
326 self.params = params
|
|
327 def copy_decl(self,indent=0):
|
|
328 return Function(self.name,self.params,indent)
|
|
329 def get_code(self):
|
|
330 str = "%sdef %s(%s):\n" % \
|
|
331 (self.currentindent(),self.name,','.join(self.params))
|
|
332 if len(self.docstr) > 0: str += self.childindent()+'"""'+self.docstr+'"""\n'
|
|
333 str += "%spass\n" % self.childindent()
|
|
334 return str
|
|
335
|
|
336 class PyParser:
|
|
337 def __init__(self):
|
|
338 self.top = Scope('global',0)
|
|
339 self.scope = self.top
|
|
340
|
|
341 def _parsedotname(self,pre=None):
|
|
342 #returns (dottedname, nexttoken)
|
|
343 name = []
|
|
344 if pre == None:
|
|
345 tokentype, token, indent = self.next()
|
|
346 if tokentype != NAME and token != '*':
|
|
347 return ('', token)
|
|
348 else: token = pre
|
|
349 name.append(token)
|
|
350 while True:
|
|
351 tokentype, token, indent = self.next()
|
|
352 if token != '.': break
|
|
353 tokentype, token, indent = self.next()
|
|
354 if tokentype != NAME: break
|
|
355 name.append(token)
|
|
356 return (".".join(name), token)
|
|
357
|
|
358 def _parseimportlist(self):
|
|
359 imports = []
|
|
360 while True:
|
|
361 name, token = self._parsedotname()
|
|
362 if not name: break
|
|
363 name2 = ''
|
|
364 if token == 'as': name2, token = self._parsedotname()
|
|
365 imports.append((name, name2))
|
|
366 while token != "," and "\n" not in token:
|
|
367 tokentype, token, indent = self.next()
|
|
368 if token != ",": break
|
|
369 return imports
|
|
370
|
|
371 def _parenparse(self):
|
|
372 name = ''
|
|
373 names = []
|
|
374 level = 1
|
|
375 while True:
|
|
376 tokentype, token, indent = self.next()
|
|
377 if token in (')', ',') and level == 1:
|
|
378 names.append(name)
|
|
379 name = ''
|
|
380 if token == '(':
|
|
381 level += 1
|
|
382 elif token == ')':
|
|
383 level -= 1
|
|
384 if level == 0: break
|
|
385 elif token == ',' and level == 1:
|
|
386 pass
|
|
387 else:
|
|
388 name += str(token)
|
|
389 return names
|
|
390
|
|
391 def _parsefunction(self,indent):
|
|
392 self.scope=self.scope.pop(indent)
|
|
393 tokentype, fname, ind = self.next()
|
|
394 if tokentype != NAME: return None
|
|
395
|
|
396 tokentype, open, ind = self.next()
|
|
397 if open != '(': return None
|
|
398 params=self._parenparse()
|
|
399
|
|
400 tokentype, colon, ind = self.next()
|
|
401 if colon != ':': return None
|
|
402
|
|
403 return Function(fname,params,indent)
|
827
|
404
|
838
|
405 def _parseclass(self,indent):
|
|
406 self.scope=self.scope.pop(indent)
|
|
407 tokentype, cname, ind = self.next()
|
|
408 if tokentype != NAME: return None
|
|
409
|
|
410 super = []
|
|
411 tokentype, next, ind = self.next()
|
|
412 if next == '(':
|
|
413 super=self._parenparse()
|
|
414 elif next != ':': return None
|
|
415
|
|
416 return Class(cname,super,indent)
|
827
|
417
|
838
|
418 def _parseassignment(self):
|
|
419 assign=''
|
|
420 tokentype, token, indent = self.next()
|
|
421 if tokentype == tokenize.STRING or token == 'str':
|
|
422 return '""'
|
|
423 elif token == '[' or token == 'list':
|
|
424 return '[]'
|
|
425 elif token == '{' or token == 'dict':
|
|
426 return '{}'
|
|
427 elif tokentype == tokenize.NUMBER:
|
|
428 return '0'
|
|
429 elif token == 'open' or token == 'file':
|
|
430 return 'file'
|
|
431 elif token == 'None':
|
|
432 return '_PyCmplNoType()'
|
|
433 elif token == 'type':
|
|
434 return 'type(_PyCmplNoType)' #only for method resolution
|
|
435 else:
|
|
436 assign += token
|
|
437 level = 0
|
|
438 while True:
|
|
439 tokentype, token, indent = self.next()
|
|
440 if token in ('(','{','['):
|
|
441 level += 1
|
|
442 elif token in (']','}',')'):
|
|
443 level -= 1
|
|
444 if level == 0: break
|
|
445 elif level == 0:
|
|
446 if token in (';','\n'): break
|
|
447 assign += token
|
|
448 return "%s" % assign
|
827
|
449
|
838
|
450 def next(self):
|
|
451 type, token, (lineno, indent), end, self.parserline = self.gen.next()
|
|
452 if lineno == self.curline:
|
|
453 #print 'line found [%s] scope=%s' % (line.replace('\n',''),self.scope.name)
|
|
454 self.currentscope = self.scope
|
|
455 return (type, token, indent)
|
|
456
|
|
457 def _adjustvisibility(self):
|
|
458 newscope = Scope('result',0)
|
|
459 scp = self.currentscope
|
|
460 while scp != None:
|
|
461 if type(scp) == Function:
|
|
462 slice = 0
|
|
463 #Handle 'self' params
|
|
464 if scp.parent != None and type(scp.parent) == Class:
|
|
465 slice = 1
|
|
466 p = scp.params[0]
|
|
467 i = p.find('=')
|
|
468 if i != -1: p = p[:i]
|
|
469 newscope.local('%s = %s' % (scp.params[0],scp.parent.name))
|
|
470 for p in scp.params[slice:]:
|
|
471 i = p.find('=')
|
|
472 if i == -1:
|
|
473 newscope.local('%s = _PyCmplNoType()' % p)
|
|
474 else:
|
|
475 newscope.local('%s = %s' % (p[:i],_sanitize(p[i+1])))
|
|
476
|
|
477 for s in scp.subscopes:
|
|
478 ns = s.copy_decl(0)
|
|
479 newscope.add(ns)
|
|
480 for l in scp.locals: newscope.local(l)
|
|
481 scp = scp.parent
|
|
482
|
|
483 self.currentscope = newscope
|
|
484 return self.currentscope
|
|
485
|
|
486 #p.parse(vim.current.buffer[:],vim.eval("line('.')"))
|
|
487 def parse(self,text,curline=0):
|
|
488 self.curline = int(curline)
|
|
489 buf = cStringIO.StringIO(''.join(text) + '\n')
|
|
490 self.gen = tokenize.generate_tokens(buf.readline)
|
|
491 self.currentscope = self.scope
|
|
492
|
827
|
493 try:
|
838
|
494 freshscope=True
|
|
495 while True:
|
|
496 tokentype, token, indent = self.next()
|
|
497 #print 'main: token=[%s] indent=[%s]' % (token,indent)
|
827
|
498
|
838
|
499 if tokentype == DEDENT:
|
|
500 self.scope = self.scope.pop(indent)
|
|
501 elif token == 'def':
|
|
502 func = self._parsefunction(indent)
|
|
503 if func == None:
|
|
504 print "function: syntax error..."
|
|
505 continue
|
|
506 freshscope = True
|
|
507 self.scope = self.scope.add(func)
|
|
508 elif token == 'class':
|
|
509 cls = self._parseclass(indent)
|
|
510 if cls == None:
|
|
511 print "class: syntax error..."
|
|
512 continue
|
|
513 freshscope = True
|
|
514 self.scope = self.scope.add(cls)
|
|
515
|
|
516 elif token == 'import':
|
|
517 imports = self._parseimportlist()
|
|
518 for mod, alias in imports:
|
|
519 loc = "import %s" % mod
|
|
520 if len(alias) > 0: loc += " as %s" % alias
|
|
521 self.scope.local(loc)
|
|
522 freshscope = False
|
|
523 elif token == 'from':
|
|
524 mod, token = self._parsedotname()
|
|
525 if not mod or token != "import":
|
|
526 print "from: syntax error..."
|
|
527 continue
|
|
528 names = self._parseimportlist()
|
|
529 for name, alias in names:
|
|
530 loc = "from %s import %s" % (mod,name)
|
|
531 if len(alias) > 0: loc += " as %s" % alias
|
|
532 self.scope.local(loc)
|
|
533 freshscope = False
|
|
534 elif tokentype == STRING:
|
|
535 if freshscope: self.scope.doc(token)
|
|
536 elif tokentype == NAME:
|
|
537 name,token = self._parsedotname(token)
|
|
538 if token == '=':
|
|
539 stmt = self._parseassignment()
|
|
540 if stmt != None:
|
|
541 self.scope.local("%s = %s" % (name,stmt))
|
|
542 freshscope = False
|
|
543 except StopIteration: #thrown on EOF
|
827
|
544 pass
|
838
|
545 except:
|
|
546 dbg("parse error: %s, %s @ %s" %
|
|
547 (sys.exc_info()[0], sys.exc_info()[1], self.parserline))
|
|
548 return self._adjustvisibility()
|
827
|
549
|
838
|
550 def _sanitize(str):
|
|
551 val = ''
|
827
|
552 level = 0
|
838
|
553 for c in str:
|
|
554 if c in ('(','{','['):
|
827
|
555 level += 1
|
838
|
556 elif c in (']','}',')'):
|
827
|
557 level -= 1
|
|
558 elif level == 0:
|
838
|
559 val += c
|
|
560 return val
|
827
|
561
|
|
562 sys.path.extend(['.','..'])
|
|
563 PYTHONEOF
|
|
564 endfunction
|
|
565
|
|
566 call s:DefPython()
|
|
567 " vim: set et ts=4:
|