blob: f0f3aaddb3d03f5144c5d275859234380f69193e [file] [log] [blame]
Bram Moolenaarbd5e15f2010-07-17 21:19:38 +02001"python3complete.vim - Omni Completion for python
2" Maintainer: Aaron Griffin <aaronmgriffin@gmail.com>
3" Version: 0.9
Bram Moolenaarca635012015-09-25 20:34:21 +02004" Last Updated: 18 Jun 2009 (small fix 2015 Sep 14 from Debian)
Bram Moolenaarbd5e15f2010-07-17 21:19:38 +02005"
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
Bram Moolenaarca635012015-09-25 20:34:21 +0200362 self.parserline = 0
Bram Moolenaarbd5e15f2010-07-17 21:19:38 +0200363
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()