Tuesday, October 11, 2011

A Vim plugin for navigating C++ with libclang

Many would say that Real Programmers should need nothing more than a text editor and a compiler, but it's hard to deny that a good IDE is a useful tool for many humans. Whether a project is large or small, I think it wouldn't be a stretch to say that an IDE can double, triple, and perhaps even quadruple productivity. Features like code completion, code generation, automatic refactoring and live syntax error highlighting can make code-writing significantly more fun and productive.

But a little-recognised fact of programming in a team is that you spend most of your time reading and understanding code written by other people. So although IDEs are usually associated with writing code, I'd say that it's their code-reading features that are even more useful. Of the code-reading features that IDEs commonly provide (amongst syntax highlighting, outline views and documentation browsing), the most useful are the code navigation features. Some examples of use-cases are:

  • Jump to the definition of a variable.
  • Find calls to a function
  • Find references to a class
  • Find subclasses of a class
  • Find references to an object

Any decent IDE will give you these code navigation features, but unfortunately, there is not a great range of C++ IDEs to choose from in Linux. This leaves a gap for text editors like Vim to rule the roost, but it is a far cry from say, Visual Studio. While there are tools like ctags and cscope that purport to provide these code navigation features, they tend to give quite bad results because the underlying problem requires the editor to have access to a fully-fledged C++ parser.

In this post, I will introduce a set of tools that I've been working on, that bring solid code navigation features to Vim, backed by a real C++ parser: Clang. Meet CLIC – the Clang Indexer for C/C++.

There are two main parts to CLIC – the indexer and the Vim plugin. The indexer works by parsing the code tree using libclang and dumping all the symbols to a database, grouped by the entity that they refer to. Once this index is built, the Vim plugin can, when queried, ask the Clang API to parse the loaded file and determine the entity under the cursor. From there, it's a simple matter to find the other references to that entity from the index.

The Indexer

If you want to try it out, you can check out and build my Github project, clang_indexer. Before you start, you'll need Clang, BerkeleyDB, Boost and zlib. If you're getting Clang as a binary package, make sure it includes the libclang.so (libclang.dylib in Mac) dynamic library.

$ git clone git://github.com/exclipy/clang_indexer.git
$ cd clang_indexer
$ mkdir build
$ cd build
$ cmake ..
$ make

This should have built three binaries: clic_add, clic_rm and clic_clear. These programs will respectively, add a file to the index, remove a file from the index, and clear the index. I don't expect these to be used directly, but they are intended to be called via the script clic_update.sh, which is in the root directory of the repo.

The first thing to do is ensure the clic_add, clic_rm and clic_clear are in your $PATH (the clic_update.sh script assumes and requires this). To build the index, it's best to create a separate directory to hold the database. So suppose you have your C++ source tree in ~/src/project/, you might want to place the index in ~/src/project-index/. Create that directory, change into it, then invoke clic_update.sh like so:

$ mkdir ~/src/project-index
$ cd ~/src/project-index
$ clic_update.sh ~/src/project

The only argument to clic_update.sh is the location of the source. This will crunch through the .cpp, .hpp, .cxx, .hxx, .cc, .c and .h files in your source tree (to change the file set, you'll just have to edit the top of clic_update.sh where the find command is invoked). When it is done, you'll see a a bunch of .i.gz files in the directory, as well as an file called index.db. The latter is the main database that the plugin will query.

The Code Navigation Plugin

To build the plugin for Vim that actually does the navigation, I piggy-backed largely on the clang_complete plugin by Rip-Rip. I've implemented the code navigation in a fork of clang_complete.

To install it, I highly suggest you use the Pathogen plugin management system so that getting updates will be as easy as a git pull. If you have Pathogen, just clone the repository into your bundle directory to install it (alternatively, use make install).

$ cd ~/.vim/bundle
$ git clone https://github.com/exclipy/clang_complete.git

Now, configure the plugin by editing your .vimrc file. At the very least, you need to enable the use of the libclang library (this is optional for the normal clang_complete plugin, but compulsory for my fork). Here are the settings that I recommend:

let g:clang_auto_select=1
let g:clang_complete_auto=0
let g:clang_complete_copen=1
let g:clang_hl_errors=1
let g:clang_periodic_quickfix=0
let g:clang_snippets=1
let g:clang_snippets_engine="clang_complete"
let g:clang_conceal_snippets=1
let g:clang_exec="clang"
let g:clang_user_options=""
let g:clang_auto_user_options="path, .clang_complete"
let g:clang_use_library=1
let g:clang_library_path="/directory/of/libclang.so/"
let g:clang_sort_algo="priority"
let g:clang_complete_macros=1
let g:clang_complete_patterns=0
nnoremap <Leader>q :call g:ClangUpdateQuickFix()<CR>

let g:clic_filename="/path/to/index.db"
nnoremap <Leader>r :call ClangGetReferences()<CR>
nnoremap <Leader>d :call ClangGetDeclarations()<CR>
nnoremap <Leader>s :call ClangGetSubclasses()<CR>

Paste the above into your .vimrc file and remember to change the paths as appropriate (clang_library_path refers to a directory while clic_filename refers to a file). The last four lines are the CLIC-specific settings for my fork of clang_complete.

Finish setting up clang_complete by setting up your .clang_complete file in your source directory. This is a newline separated list of arguments to pass to the compiler. The easiest way to get this is to build your project normally and copy the arguments that make passes to the compiler. For example, here is mine:

-I/opt/local/include
-I/opt/local/include/db52
-std=c++0x

Phew! Now that it's set up, you should be able to open one of the source files in your project with Vim. With the bindings that I've supplied in my example .vimrc settings, there are three shortcut keys for doing code navigation. If the Leader key is backslash (as is the default), then you can put the cursor over a symbol in your source and press \r to bring up a list of locations where that symbol is referenced. For example, if you do this on a function name, you'll be able to find the definition, declarations, all the call sites, and any places where you take the address of the function.

Below is a screenshot showing the plugin in action in MacVim. Here, I've put the cursor over the addMultiple function and pressed \r. The list of reference locations at the bottom of the window is clickable, and can also be navigated using :cnext, :cprev, :cfirst and :clast.

As well as \r, you can also use \d to get only declarations of a symbol – viola, we have jump-to-definition! Finally, if you focus on a class name, you can get its subclasses by pressing \s. This feature is useful if you see some code calling a method on an interface (perhaps in a dependency injected architecture) and want to know what it's actually calling through to.

The Disclaimer

Finally, please note that this is an early stage of a little project that I've been doing just to make my own life easier. As you hopefully would have gathered from these instructions, it has extremely rough edges, and I haven't really invested much effort in making it easy to use. Use at your own peril, it will destroy your code, format your hard drive, eat your children, etc. And I've only tested it in Linux and Mac. Maybe it'll work in Cygwin for Windows, but I wouldn't count on it (let me know!).


Follow the discussion on reddit and Google+.