blob: ea0a3313692ad571b86af8808f9897719c917d9a [file] [log] [blame]
Bram Moolenaarbd5e15f2010-07-17 21:19:38 +02001"python3complete.vim - Omni Completion for python
Bram Moolenaar4f4d51a2020-10-11 13:57:40 +02002" Maintainer: <vacancy>
3" Previous Maintainer: Aaron Griffin <aaronmgriffin@gmail.com>
Bram Moolenaarbd5e15f2010-07-17 21:19:38 +02004" Version: 0.9
Bram Moolenaar46eea442022-03-30 10:51:39 +01005" Last Updated: 2022 Mar 30
Bram Moolenaarbd5e15f2010-07-17 21:19:38 +02006"
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
47if !has('python3')
48 echo "Error: Required vim compiled with +python3"
49 finish
50endif
51
52function! 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
Bram Moolenaar4f4d51a2020-10-11 13:57:40 +020087 execute "py3 vimpy3complete('" . escape(cword, "'") . "', '" . escape(a:base, "'") . "')"
Bram Moolenaarbd5e15f2010-07-17 21:19:38 +020088 return g:python3complete_completions
89 endif
90endfunction
91
92function! s:DefPython()
93py3 << PYTHONEOF
Bram Moolenaar46eea442022-03-30 10:51:39 +010094import warnings
95warnings.simplefilter(action='ignore', category=FutureWarning)
96
Bram Moolenaarbd5e15f2010-07-17 21:19:38 +020097import sys, tokenize, io, types
98from token import NAME, DEDENT, NEWLINE, STRING
99
100debugstmts=[]
101def dbg(s): debugstmts.append(s)
102def showdbg():
103 for d in debugstmts: print("DBG: %s " % d)
104
105def 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
128class 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
Bram Moolenaar6c391a72021-09-09 21:55:11 +0200179 # this works for a lot of C modules that are part of the
Bram Moolenaarbd5e15f2010-07-17 21:19:38 +0200180 # 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
259class 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
328class 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
349class 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
362class PyParser:
363 def __init__(self):
364 self.top = Scope('global',0)
365 self.scope = self.top
Bram Moolenaarca635012015-09-25 20:34:21 +0200366 self.parserline = 0
Bram Moolenaarbd5e15f2010-07-17 21:19:38 +0200367
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
595def _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
607sys.path.extend(['.','..'])
608PYTHONEOF
609endfunction
610
611call s:DefPython()