├── README.rst ├── vimpy.vim └── vimpy ├── storage.py ├── tok.py └── vimpy.py /README.rst: -------------------------------------------------------------------------------- 1 | VimPy 2 | ===== 3 | 4 | This Vim plugin is to make navigation of Python files easier with Vim. If you have a large python project, navigating to multiple files may not be easy. There are various ways to make it easier like using ctags/ptags, gf over the python path etc. But it does not work well with multiple files with same name, going to a praticular class/function etc. 5 | 6 | Features 7 | ======== 8 | 9 | Currently it provides two functions - Open and Goto. 10 | Open can be used to open any python module in the project by module name, class name or function. 11 | Goto can be used to goto a module name, class name or function based on token in a file. 12 | 13 | Usage 14 | ===== 15 | Usage is similar to how ctags/ptags work in Vim. Basically there are 2 steps: 16 | 17 | 1. Create the index file. 18 | Use `:VimpyCreate path [exclude-path]` to create index. (Alternatively, the vimpy.py script can be invoked using python to create the index file. See 'python vimpy.py' for usage.) 19 | This is a one time operation. The index will be automatically updated if you edit the files using vim, otherwise this can be rerun and only the changed files will be updated. 20 | 21 | 2. Load the index file in Vim. This can be done using ':VimpyLoad ' inside Vim. This can automated by adding in the vimrc. 22 | Now it is ready to use! Following commands (the Keybindings can be changed in vimpy.vim) can be used to navigate to any desired module: 23 | 24 | - om : Open Module. Go to a module with a given name. 25 | - oc : Open Class. Go to a class with a given name. 26 | - of : Open Function. Go to a funtion with a given name. 27 | - gm : Goto Module given by word under cursor (Eg. use this to navigate to a module under an 'import' statement). 28 | - gc : Goto Class given by word under cursor. 29 | - gf : Goto Function given by word under cursor. 30 | 31 | All of them has auto completion support, so you just need to type in few characters and press . 32 | [ is typically the '\' character, but you can change it to anything (One good option is ',')]. 33 | 34 | TODO 35 | ==== 36 | 37 | There are lots of things to do. Major ones being: 38 | 39 | - Support packages 40 | - Make the goto option more intelligent 41 | -------------------------------------------------------------------------------- /vimpy.vim: -------------------------------------------------------------------------------- 1 | " Vim Plugin to make navigation across python files easy. 2 | " 3 | " Author: Amit Dev 4 | " Version: 0.1 5 | " License: This file is placed in the public domain. 6 | " 7 | 8 | if exists("g:loaded_vimpy") 9 | finish 10 | endif 11 | let g:loaded_vimpy = 1 12 | 13 | if !has('python') 14 | echo "Error: vimpy requires Vim compiled with python." 15 | finish 16 | endif 17 | 18 | " Key Bindings 19 | nnoremap om :call OpenModule() 20 | nnoremap oc :call OpenClass() 21 | nnoremap of :call OpenFun() 22 | nnoremap gm :call GotoModule() 23 | nnoremap gc :call GotoClass() 24 | nnoremap gf :call GotoFun() 25 | 26 | " Open new files in a split or buffer? 27 | let s:EditCmd = "e" 28 | 29 | " -- Implementation starts here - modify with care -- 30 | let s:bufdetails = { 'module' : ['~Module', 'Enter Module Name: ', 'CloseModule'], 31 | \ 'class' : ['~Class', 'Enter Class Name: ', 'CloseClass'], 32 | \ 'function' : ['~Function', 'Enter Function: ', 'CloseFun'] } 33 | 34 | au VimLeavePre * call s:WriteIndex() 35 | au BufWritePost *.py,*.pyx call s:UpdateIndex() 36 | 37 | python << endpython 38 | import vim 39 | import os 40 | import sys 41 | 42 | scriptdir = os.path.dirname(vim.eval('expand("")')) 43 | if not scriptdir.endswith('vimpy'): 44 | scriptdir = os.path.join(scriptdir, 'vimpy') 45 | sys.path.insert(0, scriptdir) 46 | import storage 47 | import vimpy 48 | import tok 49 | 50 | def _set_storage(path): 51 | if os.path.exists(path): 52 | st.init(path) 53 | print 'Loaded %d modules which has %d classes and %d functions.' % st.counts() 54 | else: 55 | print 'Invalid Project File' 56 | 57 | st = storage.storage('') 58 | endpython 59 | 60 | if !exists(":VimpyLoad") 61 | command -nargs=1 -complete=file VimpyLoad :python _set_storage() 62 | endif 63 | 64 | if !exists(":VimpyCreate") 65 | command -nargs=+ -complete=file VimpyCreate :python vimpy.start() 66 | endif 67 | 68 | fun! s:UpdateIndex() 69 | python << endpython 70 | if vim.current.buffer.name in st.paths: 71 | vimpy.st = st 72 | vimpy.parsefile(vim.current.buffer.name) 73 | endpython 74 | endfun 75 | 76 | fun! s:WriteIndex() 77 | python << endpython 78 | st.close() 79 | endpython 80 | endfun 81 | 82 | fun! s:GetModule(pfx) 83 | python << endpython 84 | pfx = vim.eval("a:pfx") 85 | #print 'Matching %s in %d modules' % (pfx, len(st.modules.skeys)) 86 | matches = [i for i in st.modules.skeys if i.startswith(pfx)] 87 | completions = [{'word' : i, 'menu' : st.modules.d[i]} for i in matches] 88 | vim.command("let l:res = %r" % completions) 89 | endpython 90 | return l:res 91 | endfun 92 | 93 | fun! s:GetClass(pfx) 94 | python << endpython 95 | pfx = vim.eval("a:pfx") 96 | matches = [i for i in st.classes.skeys if i.startswith(pfx)] 97 | completions = [{'word' : i, 'menu' : st.classes.d[i][0]} for i in matches] 98 | vim.command("let l:res = %r" % completions) 99 | endpython 100 | return l:res 101 | endfun 102 | 103 | fun! s:GetFun(pfx) 104 | python << endpython 105 | pfx = vim.eval("a:pfx") 106 | matches = [i for i in st.functs.skeys if i.startswith(pfx)] 107 | completions = [{'word' : i, 'menu' : st.functs.d[i][0]} for i in matches] 108 | vim.command("let l:res = %r" % completions) 109 | endpython 110 | return l:res 111 | endfun 112 | 113 | fun! s:Completer(findstart, base, fn) 114 | echo a:findstart 115 | if a:findstart 116 | let line = getline('.') 117 | let start = col('.') - 1 118 | while start > 0 && line[start - 1] =~ '[^ :]' 119 | let start -= 1 120 | endwhile 121 | return start 122 | else 123 | return call (a:fn, [a:base]) 124 | endif 125 | endfun 126 | 127 | fun! VimpyCompleteModules(findstart, base) 128 | return s:Completer(a:findstart, a:base, function('s:GetModule')) 129 | endfun 130 | 131 | fun! VimpyCompleteClasses(findstart, base) 132 | return s:Completer(a:findstart, a:base, function('s:GetClass')) 133 | endfun 134 | 135 | fun! VimpyCompleteFuns(findstart, base) 136 | return s:Completer(a:findstart, a:base, function('s:GetFun')) 137 | endfun 138 | 139 | fun! s:OpenBuf(type) 140 | let bp = s:bufdetails[a:type] 141 | exe "split " . bp[0] 142 | setlocal buftype=nofile 143 | setlocal bufhidden=hide 144 | setlocal noswapfile 145 | exe "normal i" . bp[1] 146 | call feedkeys("C") 147 | setlocal completeopt=longest,menu 148 | exe 'inoremap :call CloseBuf(function("' . bp[2] .'"))' 149 | inoremap 150 | endfun 151 | 152 | function! s:OpenClass() 153 | call s:OpenBuf('class') 154 | setlocal completefunc=VimpyCompleteClasses 155 | endfunction 156 | 157 | function! s:OpenFun() 158 | call s:OpenBuf('function') 159 | setlocal completefunc=VimpyCompleteFuns 160 | endfunction 161 | 162 | function! s:OpenModule() 163 | call s:OpenBuf('module') 164 | setlocal completefunc=VimpyCompleteModules 165 | endfunction 166 | 167 | function! s:CloseBuf(fn) 168 | let s = getline(1) 169 | let ind = stridx(s, ':') 170 | if ind != -1 171 | let name = strpart(s, ind+1) 172 | let pos = a:fn(name) 173 | if pos != '' 174 | exe "bdelete" 175 | let ind = strridx(pos, ':') 176 | let path = strpart(pos, 0, ind) 177 | let line = strpart(pos, ind+1) 178 | exe s:EditCmd . " " . path 179 | call cursor(line, 0) 180 | endif 181 | else 182 | exe "bdelete" 183 | endif 184 | endfunction 185 | 186 | function! s:CloseModule(name) 187 | let l:res = '' 188 | python << endpython 189 | k = vim.eval("a:name").strip() 190 | if k in st.modules.d: 191 | pth = st.modules.d[k] 192 | vim.command("let l:res = '%s:1'" % pth) 193 | endpython 194 | return l:res 195 | endfunction 196 | 197 | function! s:CloseClass(name) 198 | let l:res = '' 199 | python << endpython 200 | k = vim.eval("a:name").strip() 201 | if k in st.classes.d: 202 | (_, pth, line) = st.classes.d[k] 203 | #TODO: Check if moving to class name col is better 204 | vim.command("let l:res = '%s:%d'" % (pth, line)) 205 | endpython 206 | return l:res 207 | endfunction 208 | 209 | function! s:CloseFun(name) 210 | let l:res = '' 211 | python << endpython 212 | k = vim.eval("a:name").strip() 213 | if k in st.functs.d: 214 | (_, pth, line) = st.functs.d[k] 215 | #TODO: Check if moving to class name col is better 216 | vim.command("let l:res = '%s:%d'" % (pth, line)) 217 | endpython 218 | return l:res 219 | endfunction 220 | 221 | python << endpython 222 | def open_file(match, path, get): 223 | vim.command("unlet! l:res") 224 | line = vim.current.line 225 | pos = vim.current.window.cursor[1] 226 | word = tok.get_token(line, pos) 227 | if word: 228 | word = get(word) 229 | lw = len(word) 230 | matches = [i for i in match.skeys if i.startswith(word)] 231 | matches = [i for i in matches if len(i) == lw or i[lw] == ' '] 232 | if len(matches) == 1: 233 | _, pth, line = path(word) 234 | vim.command("e %s" % pth) 235 | if line: 236 | vim.current.window.cursor = (line, 0) 237 | elif len(matches) > 1: 238 | vim.command("let l:res = '%s'" % word) 239 | else: 240 | print 'No match!' 241 | else: 242 | print 'No match!' 243 | endpython 244 | 245 | function! s:GotoModule() 246 | python << endpython 247 | open_file(st.modules, 248 | lambda p: (None, st.modules.d[p], None), 249 | lambda w: "%s%s" % (w, '.py')) 250 | endpython 251 | if exists("l:res") 252 | call s:OpenModule() 253 | call feedkeys(l:res) 254 | call feedkeys("\t") 255 | endif 256 | endfunction 257 | 258 | function! s:GotoClass() 259 | python << endpython 260 | open_file(st.classes, 261 | lambda p: st.classes.d[p], 262 | lambda w: w) 263 | endpython 264 | if exists("l:res") 265 | call s:OpenClass() 266 | call feedkeys(l:res) 267 | call feedkeys("\t") 268 | endif 269 | endfunction 270 | 271 | function! s:GotoFun() 272 | python << endpython 273 | open_file(st.functs, 274 | lambda p: st.functs.d[p], 275 | lambda w: w) 276 | endpython 277 | if exists("l:res") 278 | call s:OpenFun() 279 | call feedkeys(l:res) 280 | call feedkeys("\t") 281 | endif 282 | endfunction 283 | -------------------------------------------------------------------------------- /vimpy/storage.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | import os 3 | import zipfile 4 | import cStringIO 5 | from operator import itemgetter 6 | 7 | try: 8 | import cPickle as pickle 9 | except: 10 | import pickle 11 | 12 | class DictWrapper(object): 13 | 14 | def __init__(self): 15 | self.counter = {} 16 | self.d = {} 17 | self.skeys = {} 18 | 19 | def add(self, key, val): 20 | if key in self.d and key not in self.counter: 21 | self.counter[key] = 1 22 | self.d["%s (1)" % key] = self.d[key] 23 | del self.d[key] 24 | if key in self.counter: 25 | self.counter[key] += 1 26 | key = "%s (%d)" % (key, self.counter[key]) 27 | self.d[key] = val 28 | 29 | def sort(self): 30 | self.skeys = sorted(self.d.keys()) 31 | 32 | class Entry(object): 33 | 34 | def __init__(self, name=None): 35 | self.time = 0 36 | self.module = name 37 | self.cls = [] 38 | self.funs = [] 39 | 40 | class storage(object): 41 | 42 | def __init__(self, filename): 43 | self.reset() 44 | self.paths = {} 45 | self.init(filename) 46 | self.changed = False 47 | 48 | def ismodified(self, path): 49 | mt = os.path.getmtime(path) 50 | return path not in self.paths or mt - self.paths[path].time > 1e-6 51 | 52 | def modified(self, path): 53 | if path in self.paths: 54 | self.paths[path].time = os.path.getmtime(path) 55 | self.changed = True 56 | 57 | def addfunction(self, name, module, path, line): 58 | self.paths[path].funs.append((name, line)) 59 | 60 | def addclass(self, name, module, path, line): 61 | self.paths[path].cls.append((name, line)) 62 | 63 | def addmodule(self, name, path): 64 | self.paths[path] = Entry(name) 65 | 66 | def addsub(self, subclass, superclass): 67 | self.sub[superclass].add(subclass) 68 | 69 | def resort(self): 70 | self.modules.sort() 71 | self.classes.sort() 72 | self.functs.sort() 73 | 74 | def reset(self): 75 | self.modules = DictWrapper() 76 | self.classes = DictWrapper() 77 | self.functs = DictWrapper() 78 | 79 | def _init(self): 80 | self.reset() 81 | #self.revmap = {} 82 | for path, entry in self.paths.iteritems(): 83 | #self.revmap[path] = Entry(self.modules.add(entry.module, path)) 84 | self.modules.add(entry.module, path) 85 | for c,l in entry.cls: 86 | #self.revmap[path].cls.add(self.classes.add(c, (entry.module, path, l))) 87 | self.classes.add(c, (entry.module, path, l)) 88 | for f,l in entry.funs: 89 | #self.revmap[path].funs.add(self.functs.add(f, (entry.module, path, l))) 90 | self.functs.add(f, (entry.module, path, l)) 91 | self.resort() 92 | 93 | def init(self, filename): 94 | self.filename = filename 95 | if not os.path.exists(self.filename): 96 | return 97 | try: 98 | tmpfile = os.path.basename(self.filename)+".tmp" 99 | zf = zipfile.ZipFile(self.filename, mode="r") 100 | f = cStringIO.StringIO(zf.read(tmpfile)) 101 | self.paths = pickle.load(f) 102 | self._init() 103 | except Exception,e: 104 | print 'Error while reading %r' % e 105 | self.reset() 106 | finally: 107 | try: 108 | zf.close() 109 | except Exception: 110 | pass 111 | 112 | def close(self): 113 | if not self.paths or not self.filename: 114 | return 115 | tmpfile = os.path.basename(self.filename)+".tmp" 116 | try: 117 | zf = zipfile.ZipFile(self.filename, mode="w", compression=zipfile.ZIP_DEFLATED) 118 | f = cStringIO.StringIO() 119 | pickle.dump(self.paths, f) 120 | zf.writestr(tmpfile, f.getvalue()) 121 | finally: 122 | zf.close() 123 | 124 | def counts(self): 125 | return len(self.modules.d), len(self.classes.d), len(self.functs.d) 126 | -------------------------------------------------------------------------------- /vimpy/tok.py: -------------------------------------------------------------------------------- 1 | #import ast 2 | import StringIO 3 | import tokenize 4 | import token 5 | 6 | def get_token(line, pos): 7 | for tup in tokenize.generate_tokens(StringIO.StringIO(line).readline): 8 | if tup[0] == token.NAME and pos >= tup[2][1] and pos < tup[3][1]: 9 | return tup[1].strip() 10 | return '' 11 | -------------------------------------------------------------------------------- /vimpy/vimpy.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import _ast 3 | import os 4 | import storage 5 | import compiler 6 | from compiler import visitor as ast 7 | 8 | try: 9 | import vim 10 | except: 11 | pass 12 | 13 | st = None 14 | count = 0 15 | modcount = 0 16 | errors = [] 17 | DEBUG = False 18 | from_vim = True 19 | 20 | class visitor(ast.ASTVisitor): 21 | 22 | def __init__(self, val, pth): 23 | self.module = val 24 | self.path = pth 25 | self.klass = None 26 | #print '\nAdding Module %s in %s----------' % (self.module, self.path) 27 | st.addmodule(self.module, self.path) 28 | #print 'Done Module %s in %s----------\n' % (self.module, self.path) 29 | 30 | def visitClass(self, node): 31 | self.klass = node.name 32 | #print 'Adding class %r in %s' % (node.name, self.module) 33 | st.addclass(node.name, self.module, self.path, node.lineno) 34 | #self.addSub(node) 35 | compiler.walk(node.code, self) 36 | self.klass = None 37 | 38 | def addsub(self, node): 39 | for base in node.bases: 40 | if isinstance(base, _ast.Name): 41 | st.addsub(node.name, base.id) 42 | elif isinstance(base, _ast.Attribute): 43 | st.addsub(node.name, base.attr) 44 | else: 45 | # The superclass is an expresion. Atleast log it later. 46 | pass 47 | 48 | def visitFunction(self, node): 49 | module = self.module 50 | if self.klass is not None: 51 | module = '%s :: %s' % (self.module, self.klass) 52 | #print 'Adding fun %r in %s' % (node.name, module) 53 | st.addfunction(node.name, module, self.path, node.lineno) 54 | 55 | def parsepyx(lines, filename, pth): 56 | """ Heuristic based simple pyrex/cython file parser. """ 57 | for n, line in enumerate(lines): 58 | l = line.strip() 59 | try: 60 | if (l.startswith('class') or l.startswith('cdef class')) and l[-1] == ':': 61 | end = l.find('(') 62 | st.addclass(l[l.rfind(' ',0, end)+1:end], filename, pth, n+1) 63 | elif l.startswith('def') or l.startswith('cdef'): 64 | fn = l.split('(') 65 | if fn[-1][-1] == ':': 66 | st.addfunction(fn[0].split(' ')[-1], filename, pth, n+1) 67 | except Exception: 68 | pass 69 | 70 | def parsefile(pth, ispython=True, errs=None, filename=None): 71 | if filename is None: 72 | filename = os.path.basename(pth) 73 | ispython = filename.endswith('.py') 74 | if not ispython and not filename.endswith('.pyx'): 75 | return 76 | if st.ismodified(pth): 77 | is_update = pth in st.paths 78 | if ispython: 79 | try: 80 | compiler.walk(compiler.parseFile(pth), visitor(filename, pth)) 81 | except Exception, err: 82 | if errs is not None: 83 | errs.append('Cannot Parse %s because of %r \n' % (pth, err)) 84 | else: 85 | st.addmodule(filename, pth) 86 | f = open(pth) 87 | parsepyx(f.readlines(), filename, pth) 88 | st.modified(pth) 89 | if is_update: 90 | st._init() 91 | return 1 92 | return 0 93 | 94 | def parse(filename, pth): 95 | ispython = filename.endswith('.py') 96 | if not ispython and not filename.endswith('.pyx'): 97 | return 98 | global count, modcount 99 | count += 1 100 | if not from_vim: 101 | if count != 1: 102 | sys.stdout.write('\b\b\b\b\b\b') 103 | sys.stdout.write("%06d" % count) 104 | else: 105 | vim.command('redraw | echo "Indexing %06d"' % count) 106 | modcount += parsefile(pth) 107 | 108 | def excluded(folder, exclude): 109 | fs = folder.split(os.path.sep) 110 | for exs in exclude: 111 | if len(exs) <= len(fs) and all((fs[i]==exs[i] for i in xrange(len(exs)))): 112 | return True 113 | return False 114 | 115 | def walk(folder, exclude): 116 | processed = set() 117 | for root, dirs, files in os.walk(folder, topdown=False): 118 | if root in processed or excluded(root, exclude): 119 | continue 120 | processed.add(root) 121 | for name in files: 122 | parse(name, os.path.join(root, name)) 123 | 124 | def start(prj, roots, exclude=""): 125 | global st, from_vim 126 | st = storage.storage(prj) 127 | roots = roots.split(',') 128 | if exclude: 129 | exclude = exclude.split(',') 130 | else: 131 | exclude = [] 132 | roots = [os.path.realpath(p.strip()) for p in roots] 133 | exclude = [os.path.realpath(p.strip()).split(os.path.sep) for p in exclude] 134 | if not from_vim: 135 | sys.stdout.write ("Indexing ") 136 | else: 137 | vim.command('redraw | echo "Indexing "') 138 | for p in roots: 139 | walk(p, exclude) 140 | st.close() 141 | print ' Done. Processed %d Modules, %d modules changed.' % (count, modcount) 142 | if errors: 143 | sys.stderr.write('%d modules could not be indexed because of syntax errors. Use --debug option to see details.\n' % len(errors)) 144 | if DEBUG: 145 | for error in errors: 146 | sys.stderr.write(error) 147 | 148 | if __name__ == '__main__': 149 | if len(sys.argv) < 3: 150 | print 'Usage: python %s [] [--debug]' % sys.argv[0] 151 | print ' is the result which can be used in vimpy' 152 | print ' and can be comma separated list of folders as well [Optional]' 153 | print ' --debug will show errors if any during indexing. [Optional]' 154 | exit(1) 155 | 156 | from_vim = False 157 | if len(sys.argv) >= 4: 158 | if not sys.argv[3] == '--debug': 159 | exclude = sys.argv[3] 160 | 161 | DEBUG = sys.argv[-1] == '--debug' 162 | start(sys.argv[1], sys.argv[2], exclude) 163 | --------------------------------------------------------------------------------