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