2011-06-14

Jumping to correct tags in Emacs (and in Vim)

I use Emacs to edit Python code, and navigate the code using ETags and find-tag (M-.) function. However, I noticed, that if some modules use imports like from x import foo, then sometimes (find-tag "tagname") jumps to some random file which imports a tagname, rather than to a file where tagname is defined.

I was very frustrated. Initially I thought it was a bug in etags. It appears that it is a feature. Fortunately, there is a solution which allows to find the symbol definition (scroll down to the end of the post if impatient).

Example

This is an example which reproduces the problem. Let's suppose I have three files. A.py:

from x import foo
from y import bar
    
if __name__ == "__main__":
    foo()
    bar()

x.py:

def foo():
    print "foo"

and y.py:

def bar():
    print "bar"

Then if I generate a TAGS file as

$ etags *.py
this is what it will look like:
^L
A.py,53
from x import foo^?foo^A1,0
from y import bar^?bar^A2,18
^L
x.py,19
def foo():^?foo^A1,0
^L
y.py,19
def bar():^?bar^A1,0

Now (find-tag "foo") (M-. upon foo) jumps to A.py, rather than to x.py. The problem manifests when the filename of the module using a symbol precedes alphabetically the filename of the module where the symbol is defined, but in the first place the problem is that import lines are indexed as symbol definitions.

Solution

Initially I thought it is a bug in etags, but now I think that it is a designed behaviour, though not very user-friendly.

If we see help for find-tag function, we may notice, that it has an optional next-p parameter:

(find-tag tagname &optional next-p regexp-p)

Find tag (in current tags table) whose name contains tagname.
Select the buffer containing the tag's definition, and move point there.
The default for tagname is the expression in the buffer around or before point.

If second arg next-p is t (interactively, with prefix arg), search for
another tag that matches the last tagname or regexp used.  When there are
multiple matches for a tag, more exact matches are found first.  If next-p
is the atom `-' (interactively, with prefix arg that is a negative number
or just M--), pop back to the previous tag gone to.

So, it is possible to cycle through all the positions where the tag was found by pressing M-1 M-. or Ctrl-u M-.. To cycle back, M-- M-1 M-..

I don't think it is the best solution usability-wise. While there is some value in having import positions indexed too, I'd prefer to jump always to the definition first, rather than to a random import statement. Also, the default keyboard bindings are not very ergonomic, but at least they allow to find the definition and don't require to install anything special.

P.S. Vim and ctags are affected too. To jump to the second entry of the tag, use 2 Ctrl-] rather than Ctrl-].