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