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