TIL: 'Go to Definition' in Vim for Python using Ctags, Done Right

How to set up and configure Vim to use tags for Python development so that it doesn't suck.

Install Ctags

Install the latest version of Universal Ctags, an active fork of the venerable unmaintained Exuberant Ctags.

Creating or updating tags files

You'll probably want one tags file at the root of your project, which will need to be created or updated whenever you make significant changes. Either get used to manually running the following command a lot:

ctags -R .

or bind it to a key in your ~/.vimrc:

noremap <f12> :silent !ctags -R .<CR>

I like to set Vim's current working directory equal to the root of whatever project I'm working in, so now I can press f12 to update the tags file for the project.

Test it out

Now, in Vim, ctrl-] will jump to the definition of the symbol under your text cursor. Hooray, etc. If there is more than one definition of that symbol, it presents a menu for you to choose from.

Turn off useless tags

By default, ctags generates tags for Python functions, classes, class members, variables and imports. The last two are useless to me, and they actually make ctrl-] more inconvenient, because they increase the likelyhood of finding duplicate definitions of a tag, causing the menu to inconveniently pop up, rather than just jumping to the tag you want.

To fix this, create a ~/.ctags file:

--python-kinds=-iv
--exclude=build
--exclude=dist

The first line turns off tags generation for variables and imports. The second and third lines turn off generation of tags in the named dirs, since you almost certainly want to ignore source code in those directories.

Case insensitive tag matching

If your .vimrc requests case-insensitive searching by setting ignorecase (aka ic), then the above tag matching will also be case insensitive. This is irksome, because searching for the definition of property .matrix will present you with a menu asking you to choose between property .matrix and class Matrix, rather than just jumping to the property.

To fix this, add this to your .vimrc:

" Go to defn of tag under the cursor, using case-sensitive matching,
" then restore the previous case sensitivity setting.
fun! MatchCaseTag()
    let ic = &ic
    set noic
    try
        " tjump jumps directly to a single match, or presents a menu
        exe 'tjump ' . expand('')
    finally
       let &ic = ic
    endtry
endfun
nnoremap <silent> <C-]> :call TagJumpMatchCase()<CR>

This Vim script was suggested in a comment by James Vega, in order to reliably restore the state of 'ignorecase' after doing the tag jump. Many thanks!

This maps your ctrl-] key to turn off case-insensitivity while it does the jump to tag, then turn it back on again. Now pressing ctrl-] will jump directly to your property, only presenting menus on the occasion when the tag you search for is defined in more than one place using precisely the same name.

Much better.

Update: Also see this post about adding stdlib and venv contents to your tags: https://www.fusionbox.com/blog/detail/navigating-your-django-project-with-vim-and-ctags/590/