Navigating your Django project with Vim and ctags


Posted by julian.a


Vim's built-in code navigation functionality using ctags is fantastically useful. Properly configured, it can allow you to jump from file to file in a project with one or two keypresses. Position the cursor over an object or function name, press C-] and Vim takes you directly to its definition; C-o takes you back. For multiple matches you can use g] to pull up a list. Vim's tag functionality is sophisticated and well documented; if you haven't used it before (and you use Vim) you should check it out.

For Django projects in particular, tags can be a huge boon. A moderately complex Django project can involve code in dozens of third party modules spread over hundreds of files (not to mention Django's own non-trivial code base). Figuring out exactly which method of a class based view or form you want to override in your subclass can be a royal pain in the ass. A well setup tag file can speed this process up incredibly allowing you to jump to method and object definitions with a couple button presses. Unfortunately, making ctags correctly index all your projects dependencies can be difficult.

Basic ctags setup is easy. We need to install the exuberant-ctags program (apt-get install exuberant-ctags on Debian). Enabling Vim support just requires telling Vim where to find the tags file; something like set tags=tags in our vimrc works. Then we can run:

ctags -R --fields=+l --languages=python --python-kinds=-iv -f /.tags ./

in our project root to generate our tags. This recursively indexes all python files in the current directory ignoring imports and variables (-iv) and outputs a tags file. But this won't index the standard library modules or any modules we've installed via pip. The ./ at the end of the command is just telling ctags to look in the current directory. If we want to use ctags to navigate generic class based views or similar objects, this isn't going to do us much good! To make full use a ctags, we need a way to list the paths where it should look for modules.

I was suprised to find that although there are a couple solutions floating around the internet, none of them quite work right. Assuming we're using a virtualenv, we can look at the $VIRTUAL_ENV environment variable to find our packages. This, however, fails to index the standard python libraries and also fails if you're not working in a virtualenv. A slightly more sophisticated solution is to use distutils.sysconfig.get_python_lib to find python's lib directory. This works whether or not we're using a virtualenv, but still fails to index the standard libraries. Both of these approaches fail to index packages installed via pip install -e. We could, of course, cobble together a solution combining these approaches along with some logic to find any source installed but this is getting a little baroque. We can do better.

Python, of course, keeps a list of locations where it looks for modules, so why don't we use that? sys.path is the list of directories python will look for modules. We need to do a little tweaking to output the data for the shell, but it's not too difficult:

python -c "import os, sys; print(' '.join('{}'.format(d) for d in sys.path if os.path.isdir(d)))"

putting it all together we get:

ctags -R --fields=+l --languages=python --python-kinds=-iv -f ./tags $(python -c "import os, sys; print(' '.join('{}'.format(d) for d in sys.path if os.path.isdir(d)))")

This indexes every module accessible to the current installation of python, and nothing more. It's exactly what we wanted! We could add ./ to the directories if we want to index our own work too, though personally I prefer to use ctags exclusively for navigating third party code. Obviously you don't want to be typing this command all the time. I have a keybinding in Vim to run the command; since I'm only tagging 3rd party code, I just have to remember to run it when I install new packages. If you're indexing your own work, Tim Pope has a blog post on setting up a git hook to automatically run ctags. That seems like a great idea. Either way, now you're setup to quickly and seamlessly navigate your Django code base in Vim!

P.S: Emacs users aren't totally out in the cold here. ctags can also output Emacs compatible tags! Check it out!

Return to Articles & Guides