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