Browse huge codebase with vim tags

Vim is a fantastic text editor. I really understood the benefits of it when reading “You don’t grok vim”, a famous (289 captures 2 Apr 2010 – 28 Mar 2023 in the Wayback Machine) stackoverflow post that explains the design of such tool, and its powers. Please take a moment to appreciate the dithyrambic comments.

However, when transitioning from a full-fledged c++ IDE, I missed the ability to browse symbols by going to definition, implementation or references. Here is how you give vim the ability to browse huge codebase with vim tags.

Generate tags for a given project

As pointed out in this other stackoverflow answer, ctags can only be used to list definition and implementation of a symbol. Hopefully, vim can also integrate with cscope. Here, we use gtags-cscope, as it is faster at generating and browsing yields better results for huge C++ project.

echo "preparing gtags.files for vim tags"
git ls-files --recurse-submodules  | grep .*[h\|hpp\|cpp]$ > gtags.files
find output/some_files_generated_at_compile_time>> gtags.files
gtags

echo "ctags"
ctags --c++-kinds=+pf --fields=+imaSftnlZrP --extras=+pqr --pseudo-tags='*' --languages=C++ --output-format=e-ctags --recurse=yes --sort=yes .

Configure vanilla vim

" shortcut to jump to tag for word under cursor
nnoremap t :tag <C-R>=expand("<cword>")<CR><CR>

set tagstack

"locate tag file in parent dirs:
"The last semicolon is the key here. When Vim tries to locate the 'tags' file,
"it first looks at the current directory, then the parent directory, then the
"parent of the parent, and so on. This setting works nicely with 'set
"autochdir', because then Vim's current directory is the same as the directory
"of the file.
"https://vim.fandom.com/wiki/Single_tags_file_for_a_source_tree
"vim tagfile in parent directory
set tags=tags;

" The value of 'csto' determines the order in which |:cstag| performs a search.  If 'csto' is set to zero, cscope database(s) are searched first, followed by tag file(s) if cscope did not return any matches.  If 'csto' is set to one, tag file(s) are searched before cscope database(s).  The default is zero.
" Don’t use cscope for normal tag jumps because we loose binary search, see below
set nocst

" binary search tags. this is the secrete sauce for instant browsing in huge codebases
set tagbsearch

" Use a recent version of universal-ctags, see ctags-win32 for windows releases
" https://github.com/universal-ctags/ctags-win32/releases
" Call with: ctags --c++-kinds=+pf --fields=+imaSftlnZr --extras=+qr --languages=C++ --output-format=e-ctags --recurse=yes --sort=foldcase . 
" Set tagcase to avoid a linear search.
" see :he tagbsearch /linear search can be avoided
set tagcase=match

"  The 'showfulltag' option can be used to add context from around the tag definition.
set showfulltag

Go to definition/implementation example

Thanks to the nnoremap t :tag, one just has to hover a word and hit t to jump to the definition of a tag.

Plugins can help by providing interactive tag lists, and more, see below.

Go to references example

If we want to stick to vanilla vim, we need a few preliminary steps in order to connect vim to the gtags database, as explained by this stackoverflow post. Then, you can issue :cscope find symbol MySymbol to see references of a symbol.

Here too, plugins greatly improve the whole setup, and usage experience, see below.

Plugins

Browsing with gtags-cscope

With these lines in your .vimrc file, the whole cscope setup is automated for you. One can then easily list all symbols by issuing <leader>cs, see :he gutentags_plus-keymaps for additional shortcuts.

Plugin 'ludovicchabant/vim-gutentags'
Plugin 'skywind3000/gutentags_plus'

" Disable auto generation
" Here is a bash one-liner to initialyze a new repository
" $echo "preparing gtags.files for vim tags" && git ls-files | grep .*[h\|hpp\|cpp]$ > gtags.files && gtags
let g:gutentags_add_default_project_roots = 0

" but still enable for projets who already has gtags.files
let g:gutentags_project_root = ['gtags.files']
let g:gutentags_file_list_command = {
    \ 'markers': {
      \ 'gtags.files': 'cat gtags.files',
        \ },
    \ }

let g:gutentags_modules = ['ctags', 'gtags_cscope']
let g:gutentags_ctags_extra_args = ['--c++-kinds=+pf', '--fields=+imaSftnlZrP', '--extras=+pqr', '--languages=C,C++', '--pseudo-tags=*', '--output-format=e-ctags', '--sort=yes']
" don't generate on write because big 'sort' takes forever
let g:gutentags_generate_on_write = 0
set statusline+=%{gutentags#statusline()}

Browse even faster with FZF

With those lines in your .vimrc file, when browsing to a tag with t did not bring you where you wanted, you can get an FZF interactive prompt by issuing :ts<enter>.

" FZF bindings
Plugin 'junegunn/fzf'
Plugin 'junegunn/fzf.vim'
Plugin 'zackhsi/fzf-tags'

" Bind FZF tag select to :ts<enter>
noreabbrev <expr> ts getcmdtype() == ":" && getcmdline() == 'ts' ? 'FZFTselect' : 'ts'

Conclusion

We have seen how to generate ctags and gtags for a given project, how to configure vanilla vim to take advantage of tags to navigate to definition and implementation of a given symbol and gtags-cscope to list references of this symbol.

We have also seen how plugins can automate connecting to the gtags database, refresh the tags and gtags databases, how to browse even faster.

I can say that vim is a real contender compared to IDEs when dealing with huge codebase because the sheer speed of it enables programming at the speed of mind. You don’t have to wait for spinners anymore. It is hard to imagine what a difference that can make, but once you get a taste of it, it is difficult to go back to slow full-fledged IDEs.

1 thought on “Browse huge codebase with vim tags”

Leave a Comment