1"pythoncomplete.vim - Omni Completion for python 2" Maintainer: Aaron Griffin <[email protected]> 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: 600