navigating through source code using tags

[Updated] I was always amazed by the way emacs gurus navigated through source code. It almost seemed like magic. They see some function call, and then with some short keyboard combo jump to another file, on the exact place where the function was defined.

The 'magic' in this case is a tag file. A tag file is a file that maintains an index of all the symbols in source code, where they are used and where they are defined. If you use grep and the like to look for things, you'll find tag files very useful.

There are different programs to create and deal with tag files; emacs ships with etags and there's also ctags. I'm not too familiar with those systems, but instead rely on GNU-Global for my tagging needs; it's available for Unix-like systems. The instructions below assume that you have installed GNU-Global.

tags from the command line

First, let's see how to create and update a tag file from the command line. GNU-Global works recursively on a directory tree; that is, you can run it in the top of your source directory, and it will then index all the files found, traversing through subdirectories. To create a tag file, go to the top of your source tree and run:
$ gtags
This will create a bunch of files (GPATH, GRTAGS, GSYMS and GTAGS) that together form your tagfile. If you have run gtags before, and only want to update for changes in your source files, run:
$ global -u
This is usually much faster than running gtags. Also note that you can run this from anywhere in your source tree. The program will locate the G*-files by itself.

After that, you can search for symbols etc from the command line; please refer to the global man page for details. However, let's see how we can do all that from within emacs.

Using GNU-Global from emacs

GNU-Global comes with support for emacs, which makes it very convenient to use. It still relies on the command line to use create / update the tagfiles though; but we can do something about that with a bit of elisp. First, we define the following function djcb-gtags-create-or-update in our .emacs:
(defun djcb-gtags-create-or-update ()
  "create or update the gnu global tag file"
  (if (not (= 0 (call-process "global" nil nil nil " -p"))) ; tagfile doesn't exist?
    (let ((olddir default-directory)
          (topdir (read-directory-name  
                    "gtags: top of source tree:" default-directory)))
      (cd topdir)
      (shell-command "gtags && echo 'created tagfile'")
      (cd olddir)) ; restore   
    ;;  tagfile already exists; update it
    (shell-command "global -u && echo 'updated tagfile'")))
This function will check if there is an existing tagfile; if so, it will update it. If not, it asks where to create the tag file; you should provide the name of the top of your source directory there.

Now, we can automatically run this function whenever we open a C/C++/...-file, so we always have an up-to-date tagfile available:

(add-hook 'gtags-mode-hook 
    (local-set-key (kbd "M-.") 'gtags-find-tag)   ; find a tag, also M-.
    (local-set-key (kbd "M-,") 'gtags-find-rtag)))  ; reverse tag

(add-hook 'c-mode-common-hook
  (lambda ()
    (require 'gtags)
    (gtags-mode t)
I've found GNU-Global fast enough to run djcb-gtags-create-or-update automatically. However, if you work with really huge code bases (for example, the linux kernel sources) it's better to use a tag file for some not-too-big sub-tree, or turn tagging off. To turn off automatic updating/creating tagging for some particular directory, you could do something like: [Update: fix elisp error, thanks Anatoly]
(add-hook 'c-mode-common-hook
  (lambda ()
    (require 'gtags)
    (gtags-mode t)
    (when (not (string-match "/usr/src/linux/" (expand-file-name default-directory)))  
instead of the above add-hook. You can then still run djcb-gtags-create-or-update by hand for some particular subdirectory.


Now, having done all this, tagging is quite easy - and quickly through your source code is even easier. To find the definition of a function (or symbol), type M-. (Alt + dot). If your cursor is on a function name (or on other symbol), it will be the default target. Otherwise, you type the name; autocompletion is available with the Tab-key.

If you want to do the reverse, ie. finding all the uses of a certain symbol, use M-, (Alt + comma) and we get a list of locations. There are some more functions available, all starting with gtags-; see the built-in documentation for details.


Alex Ott said...

Latest CEDET version could also take advantage of the gnu global. See http://alexott.blogspot.com/2008/12/one-more-time-about-cedet.html

Anatoly Kamchatnov said...

Minor correction:
there should be
(require 'gtags) instead of (require 'gtags-mode) in c-mode hooks' code.

djcb said...

@Anatoly: thanks, that was a silly mistake (copied out-of-context, and modified without testing...)

I updated the blog entry.

Jengu said...

Does global automatically add stuff like tags for the standard libs? If so how do you add that?

djcb said...

@Jengu: usually, you'd like to jump to source code locations, which not usually what you want for standard libraries, or?

if you're looking for information, you can of course just move the cursor to the function (say, 'printf'), and then press M-x man RET

Matthew said...

(add-hook 'gtags-mode-hook


(add-hook 'gtags-mode-hook

notice the additional apostrophe. Without this, I get a "function is nil" error.

djcb said...

@Matthew: they are subtly different, but both work for me it seems (using e23)

RC said...

I started emacs with 'emacs -q' to exclude any problems with my emacs init file. Then I evaluate:
(autoload 'gtags-mode "gtags" "" t)

and I get the response in the mini-buffer:
(autoload "gtags" "" t nil)

I next open a C++ file and run:
M-x gtags-mode
but I get the error in the mini-buffer:
Cannot open load file: gtags

I had previously run gtags in the parent directory of where the C++ file is located, and I confirmed that the GRTAGS, GPATH and GTAGS files were created and that running 'global func' is able to find func.

Could you tell me what the problem could be? Thanks.

djcb said...

@RC: it sounds like gtags is not available in your load-path. See: http://www.gnu.org/s/global/globaldoc.html#SEC32

Hope this helps.

Richard said...

Also consider ggtags package available in ELPA