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