Automatically Generate Rust ctags for Vim with rusty-tags and Gutentags

Aug 10, 2019 19:22 · 515 words · 3 minutes read

Background

This weekend I thought I’d try my hand at Rust. As always, first came some yak shaving, specifically in the form of Vim configuration. This turned out to be relatively straightforward right up until I tried to automatically generate ctags using Gutentags, like I do for all my, other projects.

Initial Configuration

Based on some snippets I found in this GitHub issue I ended up with the following config in my .vimrc:

" Setup gutentags to use rusty-tags
if !exists("g:gutentags_project_info")
  let g:gutentags_project_info = []
endif
call add(g:gutentags_project_info, {'type': 'rust', 'file': 'Cargo.toml'})
let g:gutentags_ctags_executable_rust = $HOME.'/.vim/shims/rusttags.sh'

Where $HOME.'/.vim/shims/rusttags.sh contained:

#!/bin/bash

rusty-tags vi && mv rusty-tags.vi tags.temp

Running this script manually would successfully create a tags.temp file in my Rust project root.

The Problem

I kept getting the following error in my Vim command line:

gutentags: ctags job failed, returned: 1

I ran the following in Vim to enable Gutentags tracing and trigger an update:

:call gutentags#toggletrace()
:GutentagsUpdate

This resulted in the following trace being dumped to the Vim command line:

Output from :GutentagsUpdate with trace enabled

From this, I could see that the command Gutentags was using to generate my tags was:

/home/tl/src/rcfiles/vim/.vim/bundle/vim-gutentags/plat/unix/update_tags.sh \
    -e '/home/tl/.vim/shims/rusttags.sh' \
    -t '/home/tl/.cache/gutentags/home-tl-src-foo-tags' \
    -p '/home/tl/src/foo' \
    -L 'rg --files --hidden --follow --glob "\!.git/*"' \
    -x '@/home/tl/.cache/gutentags/_wildignore.options' \
    -l '/home/tl/.cache/gutentags/home-tl-src-foo-tags.log'

Running this command in my shell directly yielded the following output:

Locking tags file...
Running file list command, patching for absolute paths
eval rg --files --hidden --follow --glob "\!.git/*"
Running ctags on whole project
/home/tl/.vim/shims/rusttags.sh -f "/home/tl/.cache/gutentags/home-tl-src-foo-tags.temp"  --exclude=@/home/tl/.cache/gutentags/_wildignore.options -L /home/tl/.cache/gutentags/home-tl-src-foo-tags.files
Fetching source and metadata ...
Creating tags for: ["foo"] ...
Replacing tags file
mv -f "/home/tl/.cache/gutentags/home-tl-src-foo-tags.temp" "/home/tl/.cache/gutentags/home-tl-src-foo-tags"
mv: cannot stat '/home/tl/.cache/gutentags/home-tl-src-foo-tags.temp': No such file or directory

After some digging I found that this was due to g:gutentags_cache_dir being set to $HOME.'/.cache/gutentags' in my .vimrc, while the script to generate the tags was assuming a default Gutentags config which generates tags within the current working directory.

The Solution

Make ~/.vim/shims/rusttags.sh smarter:

#!/bin/sh

_tmp=`getopt -o f: --long options:,exclude: -n "rusttags" -- "$@"`
eval set -- "$_tmp"

while true; do
  case "$1" in
    -f ) _output_file="${2}"; shift 2 ;;
    --options ) shift 2 ;;
    --exclude ) shift 2 ;;
    * ) break ;;
  esac
done

if [ ! -n "${_output_file}" ]; then
  echo "-f option not provided"
  exit 1
fi

echo "-f set to: ${_output_file}"

rusty-tags --output "${_output_file}" vi

This makes the script accept -f, storing the value in $_output_file.

Running the script again correctly generates takes in g:gutentags_cache_dir:

Locking tags file...
Running ctags on whole project
/home/tl/.vim/shims/rusttags.sh -f "/home/tl/.cache/gutentags/home-tl-src-foo-tags.temp"  --options=/home/tl/src/rcfiles/vim/.vim/bundle/vim-gutentags/res/ctags_recursive.options --exclude=@/home/tl/.cache/gutentags/_wildignore.options /home/tl/srcfoo
-f set to: /home/tl/.cache/gutentags/home-tl-src-foo-tags.temp
Fetching source and metadata ...
Creating tags for: ["foo"] ...
Replacing tags file
mv -f "/home/tl/.cache/gutentags/home-tl-src-foo-tags.temp" "/home/tl/.cache/gutentags/home-tl-src-foo-tags"
Unlocking tags file...
Done.

Caveats

This depends on the ordering that Gutentags currently sends these options in. This order is almost certainly not guaranteed, and if it changes (or adds a new option that rusttags.sh doesn’t support, the script could hit the * ) break ;; case before -f is handled, leaving you with broken tag generation.

This also requires GNU getopt, which if you’re running a Mac you will have to install manually.