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