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