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