Vim can be an excellent development environment for Rust code, but it isn’t amazing out of the box. As with everything in Vim, a good environment is constructed piecemeal combining various different plugins and configuring everything to work the way you like it.

This post describes how to do so, enabling modern IDE features such as:

  • Auto-completion
  • Diagnostics
  • Auto-formatting
  • Inline type and function information on demand

It’s intended as a “living document”, to be updated as my setup is. It’s currently preliminary – I haven’t used the current setup extensively, so I expect that it is far from its final form.

I’m describing my own setup, and don’t intend to make this a fully general guide which accounts for any potential set of preferences. If you follow this then feel free to pick and choose rather than following it to the letter.

Let’s get started.

Rust installation

The process of installing Rust via rustup is well documented here, so I won’t reiterate that except to note a few extra components to install:

Clippy

Clippy is a linter which can point out issues with your code which aren’t strictly speaking errors but could be expressed more cleanly, or constructs which are suspicious and may indicate bugs or misunderstandings.

Install it via rustup component add clippy.

You can then run it directly from cargo via cargo clippy.

Rustfmt

Rustfmt is a code formatter, which will automatically format your code according to a given style. Much ink has been spilled about coding style conventions so I won’t add to that except to note that just about any consistent style is better than no consistent style. I just leave it on the defaults.

Install it via rustup component add rustfmt

Other tools

There are also a few other third-party tools I install via cargo.

Bacon

Bacon is a tool for interactive development of Rust code. You leave it running in a terminal off to the side of your editor, and as you save your source files it automatically builds (or tests, lints, or whatever else you want), and displays the results. I prefer Bacon over cargo-watch, as it displays the results in a pager and hence keeps the results cleanly displayed in a consistent position, rather than appending and leaving stale results displayed above the current output.

Install it via cargo install bacon

Simply invoking bacon will run cargo check on the current project, but there are numerous options for running tests, Clippy, etc.

Vim itself

Nothing fancy. I just use the standard Debian package. If you do then make sure to install vim-nox, not vim, as vim-nox has extended scripting language support necessary for various plugins.

Vim configuration

Most of the configuration is plugin-specific. The only generic option I set is an autocommand to highlight the 100th column – the convention in Rust is a max line width of 99, so this demarcates where text can be:

au FileType rust set colorcolumn=100

Now let’s look at plugins. There are various plugin managers for installing Vim plugins. I have no idea what the current state of the art is. At any rate, plugin installation shouldn’t differ significantly between different plugin managers.

I install the following plugins for Rust development:

rust.vim

The official Rust Vim plugin, maintained by the Rust authors. This provides the basic foundation of Rust support, including Rust file detection, syntax highlighting, and more.

I configure it to automatically use rustfmt to format my code upon saving the file via:

let g:rustfmt_autosave = 1

Tagbar

Tagbar provides a sidebar which displays an outline of the contents of the current source file, which can also be used for navigation. The rust.vim plugin mentioned above integrates with Tagbar. You’ll also need to have Universal Ctags installed for this to work with Rust. On Debian it is available as the universal-ctags package.

Once it’s installed you can run :TagbarToggle to bring up the outline. By default it’s placed on the right, but I prefer it on the left, which is done by setting the following in your .vimrc:

let g:tagbar_position = 'leftabove'

YouCompleteMe

YouCompleteMe (or YCM for short) is the most feature-packed and useful of all the plugins here. It provides code completion, diagnostics, go-to-definition, and more. It uses language-specific analysis tools to parse and analyse the source code, for accurate results.

For Rust this is rust-analyzer. It’s automatically installed when you build with Rust support, so it should Just Work after installing the YCM plugin and building it via:

$ cd ~/.vim/bundle/YouCompleteMe && python3 install.py --all

YCM is very usable out of the box, with auto-completion, type signature hinting, diagnostics and more working by default. It also has many powerful commands which you’ll want to bind for ease of use. I group everything YCM-related underneath <leader>j, as it’s right under my finger.

These commands navigate to another part of the codebase, based on the context in which they are invoked. Note that the locations end up in Vim’s “jump list”, so you can navigate back to where you were before running the command via C-o, and forward again with C-i.

  • :lnext, :lprevious – these aren’t YCM commands, but YCM populates a location list for diagnostics, and these commands can be used to navigate between them. This way you don’t have to read the location from the error message and navigate there manually. I also like to put a zz after them to centre the relevant line on the screen after navigation. I bind them to <leader>jj and <leader>jk, as that mirrors the normal mode navigation keys. The commands below populate the “quickfix” list instead though, which is navigated via different commands (:cnext, :cprev, etc.), so these won’t navigate through that. I’d consider setting up bindings for those too, but it is a little annoying not to have just one set of bindings for both.
  • :YcmCompleter GoToDefinition – jump to the definition of the symbol under the cursor. I bind this to <leader>jd.
  • :YcmCompleter GoToCallers – jump to callers of the function the cursor is contained within. If there are multiple, you can select which one you want from a list. I bind this to <leader>ju (“uses”).
  • :YcmCompleter GoToReferences – jump to references of the symbol under the cursor. Similar to GoToCallers, but useful in slightly different situations. I bind this to <leader>jr.
  • <Plug>(YCMFindSymbolInWorkspace) – bring up a symbol search interface, where you can do a fuzzy search for a symbol name across your workspace, and jump to the desired result instantly. Very cool. It prioritises word boundaries, so for example if you want to find foo_bar_baz, searching for fbb will get you to a unique result quicker than just typing the whole string if the prefix is commonly used. I bind this to <leader>jf.

Informational commands

These commands display information about the symbol under the cursor. Note that for those commands that open a “preview window”, the window can be easily closed once you’re done with it via C-w z.

  • :YcmCompleter GetType – show the type of the symbol under the cursor. This is very useful since types are rarely written out explicitly in Rust, and that quite often you have multiple layers of implicitly instantiated generic types which can be hard to figure out just from reading the source . This also works on function calls to show the signature of the function. I bind this to <leader>jt.
  • :YcmCompleter GetDoc – show any documentation for the symbol under the cursor. Very convenient, but the docs aren’t formatted very nicely compared to rustdoc pages. I haven’t yet found an easy way to set up a keybind for “open the relevant rustdoc URL in my web browser”, but that would be nice. I bind this to <leader>jo.

Refactoring commands

Commands to apply source code modifications. rust-analyzer provides a rich set of “assists”, (e.g. add_impl_missing_members to add all the members required for a trait looks really handy) but unfortunately these aren’t exposed through YCM. There may be a way to invoke them by sending some custom LSP message or through another plugin, but I haven’t looked into that yet. It does expose a couple of useful tools though:

  • :YcmCompleter FixIt – apply any fixes the compiler has generated for the diagnostic under the cursor. I bind this to <leader>jx.
  • YcmCompleter Refactorname <new name> – rename the identifier under the cursor, everywhere it is used. I currently haven’t bound this to anything as I’m not sure I’ll use it frequently enough, but it’s good to know the command so you can use it manually.

Note that when an edit is applied to multiple files you have to run :wa to save the files you don’t have open, as YCM opens them in a hidden window to make the edit.

In summary, the full list of bindings I’ve set up is:

nnoremap <leader>jd :YcmCompleter GoToDefinition<CR>
nnoremap <leader>ju :YcmCompleter GoToCallers<CR>
nnoremap <leader>jr :YcmCompleter GoToReferences<CR>
nmap <leader>jf <Plug>(YCMFindSymbolInWorkspace)
nnoremap <Leader>jk :lprevious<CR>zz
nnoremap <Leader>jj :lnext<CR>zz
nnoremap <leader>jt :YcmCompleter GetType<CR>
nnoremap <leader>jo :YcmCompleter GetDoc<CR>
nnoremap <leader>jx :YcmCompleter FixIt<CR>

Other features

  • :YcmDiags brings up a window at the bottom containing the current diagnostics. You can navigate through them without this window open, and I like having the full diagnostics visible in bacon anyway, so I don’t currently use this.
  • Auto-hover. By default, after a short period of inactivity a popup will appear containing info for the symbol your cursor is over. I have a feeling this may get annoying, obscuring the code I’m trying to read. It gives voluminous detail about language keywords if you happen to be sitting on them, which seems unhelpful. I may end up disabling this via g:ycm_auto_hover. It can also be manually toggled via the mapping <Plug>(YCMHover), which may be a better option.

Conclusion

That’s it! Altogether this provides a very nice development environment, much better than I typically have access to in Vim. Now time to use it to write some actual code…