1"python3complete.vim - Omni Completion for python
2" Maintainer: Aaron Griffin <[email protected]>
3" Version: 0.9
4" Last Updated: 18 Jun 2009 (small fix 2015 Sep 14 from Debian)
5"
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
362        self.parserline = 0
363
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()
608