Rust development with Vim
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.
Navigation commands
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 azz
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 toGoToCallers
, 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 findfoo_bar_baz
, searching forfbb
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 torustdoc
pages. I haven’t yet found an easy way to set up a keybind for “open the relevantrustdoc
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 inbacon
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…