123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612 |
- "python3complete.vim - Omni Completion for python
- " Maintainer: <vacancy>
- " Previous Maintainer: Aaron Griffin <aaronmgriffin@gmail.com>
- " Version: 0.9
- " Last Updated: 2022 Mar 30
- "
- " Roland Puntaier: this file contains adaptations for python3 and is parallel to pythoncomplete.vim
- "
- " Changes
- " TODO:
- " 'info' item output can use some formatting work
- " Add an "unsafe eval" mode, to allow for return type evaluation
- " Complete basic syntax along with import statements
- " i.e. "import url<c-x,c-o>"
- " Continue parsing on invalid line??
- "
- " v 0.9
- " * Fixed docstring parsing for classes and functions
- " * Fixed parsing of *args and **kwargs type arguments
- " * Better function param parsing to handle things like tuples and
- " lambda defaults args
- "
- " v 0.8
- " * Fixed an issue where the FIRST assignment was always used instead of
- " using a subsequent assignment for a variable
- " * Fixed a scoping issue when working inside a parameterless function
- "
- "
- " v 0.7
- " * Fixed function list sorting (_ and __ at the bottom)
- " * Removed newline removal from docs. It appears vim handles these better in
- " recent patches
- "
- " v 0.6:
- " * Fixed argument completion
- " * Removed the 'kind' completions, as they are better indicated
- " with real syntax
- " * Added tuple assignment parsing (whoops, that was forgotten)
- " * Fixed import handling when flattening scope
- "
- " v 0.5:
- " Yeah, I skipped a version number - 0.4 was never public.
- " It was a bugfix version on top of 0.3. This is a complete
- " rewrite.
- "
- if !has('python3')
- echo "Error: Required vim compiled with +python3"
- finish
- endif
- function! python3complete#Complete(findstart, base)
- "findstart = 1 when we need to get the text length
- if a:findstart == 1
- let line = getline('.')
- let idx = col('.')
- while idx > 0
- let idx -= 1
- let c = line[idx]
- if c =~ '\w'
- continue
- elseif ! c =~ '\.'
- let idx = -1
- break
- else
- break
- endif
- endwhile
- return idx
- "findstart = 0 when we need to return the list of completions
- else
- "vim no longer moves the cursor upon completion... fix that
- let line = getline('.')
- let idx = col('.')
- let cword = ''
- while idx > 0
- let idx -= 1
- let c = line[idx]
- if c =~ '\w' || c =~ '\.'
- let cword = c . cword
- continue
- elseif strlen(cword) > 0 || idx == 0
- break
- endif
- endwhile
- execute "py3 vimpy3complete('" . escape(cword, "'") . "', '" . escape(a:base, "'") . "')"
- return g:python3complete_completions
- endif
- endfunction
- function! s:DefPython()
- py3 << PYTHONEOF
- import warnings
- warnings.simplefilter(action='ignore', category=FutureWarning)
- import sys, tokenize, io, types
- from token import NAME, DEDENT, NEWLINE, STRING
- debugstmts=[]
- def dbg(s): debugstmts.append(s)
- def showdbg():
- for d in debugstmts: print("DBG: %s " % d)
- def vimpy3complete(context,match):
- global debugstmts
- debugstmts = []
- try:
- import vim
- cmpl = Completer()
- cmpl.evalsource('\n'.join(vim.current.buffer),vim.eval("line('.')"))
- all = cmpl.get_completions(context,match)
- all.sort(key=lambda x:x['abbr'].replace('_','z'))
- dictstr = '['
- # have to do this for double quoting
- for cmpl in all:
- dictstr += '{'
- for x in cmpl: dictstr += '"%s":"%s",' % (x,cmpl[x])
- dictstr += '"icase":0},'
- if dictstr[-1] == ',': dictstr = dictstr[:-1]
- dictstr += ']'
- #dbg("dict: %s" % dictstr)
- vim.command("silent let g:python3complete_completions = %s" % dictstr)
- #dbg("Completion dict:\n%s" % all)
- except vim.error:
- dbg("VIM Error: %s" % vim.error)
- class Completer(object):
- def __init__(self):
- self.compldict = {}
- self.parser = PyParser()
- def evalsource(self,text,line=0):
- sc = self.parser.parse(text,line)
- src = sc.get_code()
- dbg("source: %s" % src)
- try: exec(src,self.compldict)
- except: dbg("parser: %s, %s" % (sys.exc_info()[0],sys.exc_info()[1]))
- for l in sc.locals:
- try: exec(l,self.compldict)
- except: dbg("locals: %s, %s [%s]" % (sys.exc_info()[0],sys.exc_info()[1],l))
- def _cleanstr(self,doc):
- return doc.replace('"',' ').replace("'",' ')
- def get_arguments(self,func_obj):
- def _ctor(class_ob):
- try: return class_ob.__init__
- except AttributeError:
- for base in class_ob.__bases__:
- rc = _ctor(base)
- if rc is not None: return rc
- return None
- arg_offset = 1
- if type(func_obj) == type: func_obj = _ctor(func_obj)
- elif type(func_obj) == types.MethodType: arg_offset = 1
- else: arg_offset = 0
- arg_text=''
- if type(func_obj) in [types.FunctionType, types.LambdaType,types.MethodType]:
- try:
- cd = func_obj.__code__
- real_args = cd.co_varnames[arg_offset:cd.co_argcount]
- defaults = func_obj.__defaults__ or []
- defaults = ["=%s" % name for name in defaults]
- defaults = [""] * (len(real_args)-len(defaults)) + defaults
- items = [a+d for a,d in zip(real_args,defaults)]
- if func_obj.__code__.co_flags & 0x4:
- items.append("...")
- if func_obj.__code__.co_flags & 0x8:
- items.append("***")
- arg_text = (','.join(items)) + ')'
- except:
- dbg("arg completion: %s: %s" % (sys.exc_info()[0],sys.exc_info()[1]))
- pass
- if len(arg_text) == 0:
- # The doc string sometimes contains the function signature
- # this works for a lot of C modules that are part of the
- # standard library
- doc = func_obj.__doc__
- if doc:
- doc = doc.lstrip()
- pos = doc.find('\n')
- if pos > 0:
- sigline = doc[:pos]
- lidx = sigline.find('(')
- ridx = sigline.find(')')
- if lidx > 0 and ridx > 0:
- arg_text = sigline[lidx+1:ridx] + ')'
- if len(arg_text) == 0: arg_text = ')'
- return arg_text
- def get_completions(self,context,match):
- #dbg("get_completions('%s','%s')" % (context,match))
- stmt = ''
- if context: stmt += str(context)
- if match: stmt += str(match)
- try:
- result = None
- all = {}
- ridx = stmt.rfind('.')
- if len(stmt) > 0 and stmt[-1] == '(':
- result = eval(_sanitize(stmt[:-1]), self.compldict)
- doc = result.__doc__
- if doc is None: doc = ''
- args = self.get_arguments(result)
- return [{'word':self._cleanstr(args),'info':self._cleanstr(doc)}]
- elif ridx == -1:
- match = stmt
- all = self.compldict
- else:
- match = stmt[ridx+1:]
- stmt = _sanitize(stmt[:ridx])
- result = eval(stmt, self.compldict)
- all = dir(result)
- dbg("completing: stmt:%s" % stmt)
- completions = []
- try: maindoc = result.__doc__
- except: maindoc = ' '
- if maindoc is None: maindoc = ' '
- for m in all:
- if m == "_PyCmplNoType": continue #this is internal
- try:
- dbg('possible completion: %s' % m)
- if m.find(match) == 0:
- if result is None: inst = all[m]
- else: inst = getattr(result,m)
- try: doc = inst.__doc__
- except: doc = maindoc
- typestr = str(inst)
- if doc is None or doc == '': doc = maindoc
- wrd = m[len(match):]
- c = {'word':wrd, 'abbr':m, 'info':self._cleanstr(doc)}
- if "function" in typestr:
- c['word'] += '('
- c['abbr'] += '(' + self._cleanstr(self.get_arguments(inst))
- elif "method" in typestr:
- c['word'] += '('
- c['abbr'] += '(' + self._cleanstr(self.get_arguments(inst))
- elif "module" in typestr:
- c['word'] += '.'
- elif "type" in typestr:
- c['word'] += '('
- c['abbr'] += '('
- completions.append(c)
- except:
- i = sys.exc_info()
- dbg("inner completion: %s,%s [stmt='%s']" % (i[0],i[1],stmt))
- return completions
- except:
- i = sys.exc_info()
- dbg("completion: %s,%s [stmt='%s']" % (i[0],i[1],stmt))
- return []
- class Scope(object):
- def __init__(self,name,indent,docstr=''):
- self.subscopes = []
- self.docstr = docstr
- self.locals = []
- self.parent = None
- self.name = name
- self.indent = indent
- def add(self,sub):
- #print('push scope: [%s@%s]' % (sub.name,sub.indent))
- sub.parent = self
- self.subscopes.append(sub)
- return sub
- def doc(self,str):
- """ Clean up a docstring """
- d = str.replace('\n',' ')
- d = d.replace('\t',' ')
- while d.find(' ') > -1: d = d.replace(' ',' ')
- while d[0] in '"\'\t ': d = d[1:]
- while d[-1] in '"\'\t ': d = d[:-1]
- dbg("Scope(%s)::docstr = %s" % (self,d))
- self.docstr = d
- def local(self,loc):
- self._checkexisting(loc)
- self.locals.append(loc)
- def copy_decl(self,indent=0):
- """ Copy a scope's declaration only, at the specified indent level - not local variables """
- return Scope(self.name,indent,self.docstr)
- def _checkexisting(self,test):
- "Convienance function... keep out duplicates"
- if test.find('=') > -1:
- var = test.split('=')[0].strip()
- for l in self.locals:
- if l.find('=') > -1 and var == l.split('=')[0].strip():
- self.locals.remove(l)
- def get_code(self):
- str = ""
- if len(self.docstr) > 0: str += '"""'+self.docstr+'"""\n'
- for l in self.locals:
- if l.startswith('import'): str += l+'\n'
- str += 'class _PyCmplNoType:\n def __getattr__(self,name):\n return None\n'
- for sub in self.subscopes:
- str += sub.get_code()
- for l in self.locals:
- if not l.startswith('import'): str += l+'\n'
- return str
- def pop(self,indent):
- #print('pop scope: [%s] to [%s]' % (self.indent,indent))
- outer = self
- while outer.parent != None and outer.indent >= indent:
- outer = outer.parent
- return outer
- def currentindent(self):
- #print('parse current indent: %s' % self.indent)
- return ' '*self.indent
- def childindent(self):
- #print('parse child indent: [%s]' % (self.indent+1))
- return ' '*(self.indent+1)
- class Class(Scope):
- def __init__(self, name, supers, indent, docstr=''):
- Scope.__init__(self,name,indent, docstr)
- self.supers = supers
- def copy_decl(self,indent=0):
- c = Class(self.name,self.supers,indent, self.docstr)
- for s in self.subscopes:
- c.add(s.copy_decl(indent+1))
- return c
- def get_code(self):
- str = '%sclass %s' % (self.currentindent(),self.name)
- if len(self.supers) > 0: str += '(%s)' % ','.join(self.supers)
- str += ':\n'
- if len(self.docstr) > 0: str += self.childindent()+'"""'+self.docstr+'"""\n'
- if len(self.subscopes) > 0:
- for s in self.subscopes: str += s.get_code()
- else:
- str += '%spass\n' % self.childindent()
- return str
- class Function(Scope):
- def __init__(self, name, params, indent, docstr=''):
- Scope.__init__(self,name,indent, docstr)
- self.params = params
- def copy_decl(self,indent=0):
- return Function(self.name,self.params,indent, self.docstr)
- def get_code(self):
- str = "%sdef %s(%s):\n" % \
- (self.currentindent(),self.name,','.join(self.params))
- if len(self.docstr) > 0: str += self.childindent()+'"""'+self.docstr+'"""\n'
- str += "%spass\n" % self.childindent()
- return str
- class PyParser:
- def __init__(self):
- self.top = Scope('global',0)
- self.scope = self.top
- self.parserline = 0
- def _parsedotname(self,pre=None):
- #returns (dottedname, nexttoken)
- name = []
- if pre is None:
- tokentype, token, indent = self.donext()
- if tokentype != NAME and token != '*':
- return ('', token)
- else: token = pre
- name.append(token)
- while True:
- tokentype, token, indent = self.donext()
- if token != '.': break
- tokentype, token, indent = self.donext()
- if tokentype != NAME: break
- name.append(token)
- return (".".join(name), token)
- def _parseimportlist(self):
- imports = []
- while True:
- name, token = self._parsedotname()
- if not name: break
- name2 = ''
- if token == 'as': name2, token = self._parsedotname()
- imports.append((name, name2))
- while token != "," and "\n" not in token:
- tokentype, token, indent = self.donext()
- if token != ",": break
- return imports
- def _parenparse(self):
- name = ''
- names = []
- level = 1
- while True:
- tokentype, token, indent = self.donext()
- if token in (')', ',') and level == 1:
- if '=' not in name: name = name.replace(' ', '')
- names.append(name.strip())
- name = ''
- if token == '(':
- level += 1
- name += "("
- elif token == ')':
- level -= 1
- if level == 0: break
- else: name += ")"
- elif token == ',' and level == 1:
- pass
- else:
- name += "%s " % str(token)
- return names
- def _parsefunction(self,indent):
- self.scope=self.scope.pop(indent)
- tokentype, fname, ind = self.donext()
- if tokentype != NAME: return None
- tokentype, open, ind = self.donext()
- if open != '(': return None
- params=self._parenparse()
- tokentype, colon, ind = self.donext()
- if colon != ':': return None
- return Function(fname,params,indent)
- def _parseclass(self,indent):
- self.scope=self.scope.pop(indent)
- tokentype, cname, ind = self.donext()
- if tokentype != NAME: return None
- super = []
- tokentype, thenext, ind = self.donext()
- if thenext == '(':
- super=self._parenparse()
- elif thenext != ':': return None
- return Class(cname,super,indent)
- def _parseassignment(self):
- assign=''
- tokentype, token, indent = self.donext()
- if tokentype == tokenize.STRING or token == 'str':
- return '""'
- elif token == '(' or token == 'tuple':
- return '()'
- elif token == '[' or token == 'list':
- return '[]'
- elif token == '{' or token == 'dict':
- return '{}'
- elif tokentype == tokenize.NUMBER:
- return '0'
- elif token == 'open' or token == 'file':
- return 'file'
- elif token == 'None':
- return '_PyCmplNoType()'
- elif token == 'type':
- return 'type(_PyCmplNoType)' #only for method resolution
- else:
- assign += token
- level = 0
- while True:
- tokentype, token, indent = self.donext()
- if token in ('(','{','['):
- level += 1
- elif token in (']','}',')'):
- level -= 1
- if level == 0: break
- elif level == 0:
- if token in (';','\n'): break
- assign += token
- return "%s" % assign
- def donext(self):
- type, token, (lineno, indent), end, self.parserline = next(self.gen)
- if lineno == self.curline:
- #print('line found [%s] scope=%s' % (line.replace('\n',''),self.scope.name))
- self.currentscope = self.scope
- return (type, token, indent)
- def _adjustvisibility(self):
- newscope = Scope('result',0)
- scp = self.currentscope
- while scp != None:
- if type(scp) == Function:
- slice = 0
- #Handle 'self' params
- if scp.parent != None and type(scp.parent) == Class:
- slice = 1
- newscope.local('%s = %s' % (scp.params[0],scp.parent.name))
- for p in scp.params[slice:]:
- i = p.find('=')
- if len(p) == 0: continue
- pvar = ''
- ptype = ''
- if i == -1:
- pvar = p
- ptype = '_PyCmplNoType()'
- else:
- pvar = p[:i]
- ptype = _sanitize(p[i+1:])
- if pvar.startswith('**'):
- pvar = pvar[2:]
- ptype = '{}'
- elif pvar.startswith('*'):
- pvar = pvar[1:]
- ptype = '[]'
- newscope.local('%s = %s' % (pvar,ptype))
- for s in scp.subscopes:
- ns = s.copy_decl(0)
- newscope.add(ns)
- for l in scp.locals: newscope.local(l)
- scp = scp.parent
- self.currentscope = newscope
- return self.currentscope
- #p.parse(vim.current.buffer[:],vim.eval("line('.')"))
- def parse(self,text,curline=0):
- self.curline = int(curline)
- buf = io.StringIO(''.join(text) + '\n')
- self.gen = tokenize.generate_tokens(buf.readline)
- self.currentscope = self.scope
- try:
- freshscope=True
- while True:
- tokentype, token, indent = self.donext()
- #dbg( 'main: token=[%s] indent=[%s]' % (token,indent))
- if tokentype == DEDENT or token == "pass":
- self.scope = self.scope.pop(indent)
- elif token == 'def':
- func = self._parsefunction(indent)
- if func is None:
- print("function: syntax error...")
- continue
- dbg("new scope: function")
- freshscope = True
- self.scope = self.scope.add(func)
- elif token == 'class':
- cls = self._parseclass(indent)
- if cls is None:
- print("class: syntax error...")
- continue
- freshscope = True
- dbg("new scope: class")
- self.scope = self.scope.add(cls)
-
- elif token == 'import':
- imports = self._parseimportlist()
- for mod, alias in imports:
- loc = "import %s" % mod
- if len(alias) > 0: loc += " as %s" % alias
- self.scope.local(loc)
- freshscope = False
- elif token == 'from':
- mod, token = self._parsedotname()
- if not mod or token != "import":
- print("from: syntax error...")
- continue
- names = self._parseimportlist()
- for name, alias in names:
- loc = "from %s import %s" % (mod,name)
- if len(alias) > 0: loc += " as %s" % alias
- self.scope.local(loc)
- freshscope = False
- elif tokentype == STRING:
- if freshscope: self.scope.doc(token)
- elif tokentype == NAME:
- name,token = self._parsedotname(token)
- if token == '=':
- stmt = self._parseassignment()
- dbg("parseassignment: %s = %s" % (name, stmt))
- if stmt != None:
- self.scope.local("%s = %s" % (name,stmt))
- freshscope = False
- except StopIteration: #thrown on EOF
- pass
- except:
- dbg("parse error: %s, %s @ %s" %
- (sys.exc_info()[0], sys.exc_info()[1], self.parserline))
- return self._adjustvisibility()
- def _sanitize(str):
- val = ''
- level = 0
- for c in str:
- if c in ('(','{','['):
- level += 1
- elif c in (']','}',')'):
- level -= 1
- elif level == 0:
- val += c
- return val
- sys.path.extend(['.','..'])
- PYTHONEOF
- endfunction
- call s:DefPython()
|