Setting up Neovim for Rust development
Before starting on my Rust UO server implementation I needed to set up my editor to allow me to be as productive as possible. I’ve been using Neovim for a couple of years now but hadn’t bothered setting it up properly for Rust until now as I’d only been playing around with noddy example code while reading The Rust Book.
I started from a fresh install of Neovim, completely vanilla. First step was to install a plugin manager:
Plugin & package managers
Previously I’d used packer.nvim and vim-plug to manage Neovim plugins but the latest and greatest now seems to be lazy.nvim so I decided to try it.
I took the installation snippet from the readme and added it to a plugins module that I required from my init file:
-- ~/.config/nvim/init.lua
require('plugins')
-- ~/.config/nvim/lua/plugins.lua
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
vim.fn.system({
"git",
"clone",
"--filter=blob:none",
"https://github.com/folke/lazy.nvim.git",
"--branch=stable", -- latest stable release
lazypath,
})
end
vim.opt.rtp:prepend(lazypath)
require("lazy").setup(require('plugins.specs'))
The require
on the last line above calls Lazy’s setup function, passing it an empty plugin specs table required from a nested specs module I created:
-- ~/.config/nvim/lua/plugins/specs.lua
return {}
Next I installed mason.nvim, a plugin that makes managing and installing LSP servers easier. To install it I added it to the table in the newly created specs module”
-- ~/.config/nvim/lua/plugins/specs.lua
return {
{ "williamboman/mason.nvim" },
{ "williamboman/mason-lspconfig.nvim" },
{ "neovim/nvim-lspconfig" },
}
mason-lspconfig.nvim and nvim-lspconfig were recommended to be installed alongside Mason.
I added the setup function calls needed to make Mason work to a new nested setup module.
-- ~/.config/nvim/lua/plugins/setup/mason.lua
require("mason").setup()
require("mason-lspconfig").setup()
And required it from the plugins module:
-- ~/.config/nvim/lua/plugins.lua
require('plugins.setup.mason')
After installing the plugin and package managers it was time to get an LSP server installed and configured:
LSP server
The LSP server I chose to install was rust-analyzer. Installation was easy with Mason, I just needed to run the following command in Neovim and the LSP server and dependencies were installed:
:MasonInstall rust-analyzer
Installing the LSP server makes the following possible:
- Code completion
- Refactoring
- Linting
- Go to definition and references
- Code actions
- Access to documentation
- Snippets
- Improved syntax highlighting
- Formatting
Next I needed to configure rust-analyzer to work with lspconfig. The easiest way to do that was to install rust-tools.nvim:
-- ~/.config/nvim/lua/plugins/specs.lua
return {
-- snip
{ "simrat39/rust-tools.nvim" },
}
And then call the setup function and configure the keymaps:
-- ~/.config/nvim/lua/plugins.lua
require('plugins.setup.rust-tools')
-- ~/.config/nvim/lua/plugins/setup/rust-tools.lua
local rt = require("rust-tools")
rt.setup({
server = {
on_attach = function(_, bufnr)
vim.keymap.set("n", "<C-space>", rt.hover_actions.hover_actions, { buffer = bufnr })
vim.keymap.set("n", "<Leader>a", rt.code_action_group.code_action_group, { buffer = bufnr })
end,
},
})
The keymaps above set up Ctrl-Space to display a floating window above the symbol under the cursor with information on it, and
Some configuration was also needed. I added this to a new lsp-opts module, required from init.lua:
-- ~/.config/nvim/init.lua
require('lsp-opts')
-- ~/.config/nvim/lua/lsp-opts.lua
vim.diagnostic.config({
virtual_text = false,
signs = true,
update_in_insert = true,
underline = true,
severity_sort = false,
float = {
border = 'rounded',
source = 'always',
header = '',
prefix = '',
},
})
vim.cmd([[
set signcolumn=yes
autocmd CursorHold * lua vim.diagnostic.open_float(nil, { focusable = false })
]])
The above configures the LSP floating diagnostic window and sets it to appear immediately when the cursor is stationary over a symbol.
Next I tackled getting a debugger working with Rust:
Debugger
I opted to install the codelldb debugger. Installation was via Mason:
:MasonInstall codelldb
To be able to use the debugger, I installed the vimspector plugin which provides a debugging UI:
-- ~/.config/nvim/lua/plugins/specs.lua
return {
-- snip
{ "puremourning/vimspector" },
}
And then configured the UI and keymaps:
-- ~/.config/nvim/init.lua
require('debugging-opts')
-- ~/.config/nvim/lua/debugging-opts.lua
vim.cmd([[
let g:vimspector_sidebar_width = 85
let g:vimspector_bottombar_height = 15
let g:vimspector_terminal_maxwidth = 70
]])
vim.cmd([[
nnoremap <Leader>dd :call vimspector#Launch()<CR>
nnoremap <Leader>de :call vimspector#Reset()<CR>
nnoremap <Leader>dc :call vimspector#Continue()<CR>
nnoremap <Leader>dt :call vimspector#ToggleBreakpoint()<CR>
nnoremap <Leader>dT :call vimspector#ClearBreakpoints()<CR>
nmap <Leader>dk <Plug>VimspectorRestart
nmap <Leader>dh <Plug>VimspectorStepOut
nmap <Leader>dl <Plug>VimspectorStepInto
nmap <Leader>dj <Plug>VimspectorStepOver
]])
To use the debugger in a Rust project I also needed to create a Vimspector config file:
APPNAME/.vimspector.json:
{
"configurations": {
"launch": {
"adapter": "CodeLLDB",
"filetypes": [ "rust" ],
"configuration": {
"request": "launch",
"program": "${workspaceRoot}/target/debug/APPNAME"
}
}
}
}
APPNAME
would be changed to the name of the project e.g. foo_project
if cargo new foo_project
had been run.
Next up was a code completion framework and sources:
Completion framework & sources
I installed the nvim-cmp completion framework:
-- ~/.config/nvim/lua/plugins/specs.lua
return {
-- snip
{ "hrsh7th/nvim-cmp" },
}
And the following sources:
-- ~/.config/nvim/lua/plugins/specs.lua
return {
-- snip
{ "hrsh7th/cmp-nvim-lsp" },
{ "hrsh7th/cmp-nvim-lua" },
{ "hrsh7th/cmp-nvim-lsp-signature-help" },
{ "hrsh7th/cmp-vsnip" },
{ "hrsh7th/cmp-path" },
{ "hrsh7th/cmp-buffer" },
{ "hrsh7th/vim-vsnip" },
}
I added the call to cmp’s setup function (and the configuration passed to it) in a new setup module for cmp, required from the plugins module:
-- ~/.config/nvim/lua/plugins.lua
require('plugins.setup.cmp')
-- ~/.config/nvim/lua/plugins/setup/cmp.lua
local cmp = require'cmp'
cmp.setup({
snippet = {
expand = function(args)
vim.fn["vsnip#anonymous"](args.body)
end,
},
sources = {
{ name = 'path' },
{ name = 'nvim_lsp', keyword_length = 3 },
{ name = 'nvim_lsp_signature_help'},
{ name = 'nvim_lua', keyword_length = 2},
{ name = 'buffer', keyword_length = 2 },
{ name = 'vsnip', keyword_length = 2 },
{ name = 'calc'},
},
window = {
completion = cmp.config.window.bordered(),
documentation = cmp.config.window.bordered(),
},
formatting = {
fields = {'menu', 'abbr', 'kind'},
format = function(entry, item)
local menu_icon ={
nvim_lsp = 'λ',
vsnip = '⋗',
buffer = 'Ω',
path = '🖫',
}
item.menu = menu_icon[entry.source.name]
return item
end,
},
mapping = {
['<C-p>'] = cmp.mapping.select_prev_item(),
['<C-n>'] = cmp.mapping.select_next_item(),
['<S-Tab>'] = cmp.mapping.select_prev_item(),
['<Tab>'] = cmp.mapping.select_next_item(),
['<C-S-f>'] = cmp.mapping.scroll_docs(-4),
['<C-f>'] = cmp.mapping.scroll_docs(4),
['<C-Space>'] = cmp.mapping.complete(),
['<C-e>'] = cmp.mapping.close(),
['<CR>'] = cmp.mapping.confirm({
behavior = cmp.ConfirmBehavior.Insert,
select = true,
})
},
})
I set additional options in a new options module, required from init.lua:
-- ~/.config/nvim/init.lua
require('completion-opts')
-- ~/.config/nvim/lua/completion-opts.lua
vim.opt.completeopt = {'menuone', 'noselect', 'noinsert'}
vim.opt.shortmess = vim.opt.shortmess + { c = true}
vim.api.nvim_set_option('updatetime', 300)
Next I configured a parser:
Parser & syntax highlighting
The parser I installed was nvim-treesitter. Treesitter is an incremental parser so the improved syntax highlighting it gives you even shows up when there is invalid code elsewhere in the buffer.
I installed it with Lazy by adding it to the specs module:”
-- ~/.config/nvim/lua/plugins/specs.lua
return {
-- snip
{ "nvim-treesitter/nvim-treesitter" },
}
And called its setup function in a new setup module, required from the plugins module:
-- ~/.config/nvim/lua/plugins.lua
require('plugins.setup.treesitter')
-- ~/.config/nvim/lua/plugins/setup/treesitter.lua
require('nvim-treesitter.configs').setup {
ensure_installed = { "lua", "rust", "toml" },
auto_install = true,
highlight = {
enable = true,
additional_vim_regex_highlighting=false,
},
ident = { enable = true },
rainbow = {
enable = true,
extended_mode = true,
max_file_lines = nil,
}
}
After this I needed to run the following command in Neovim to verify that the parsers for Rust, TOML and Lua had been installed:
:TSInstallInfo
I then configured Neovim to perform folding with treesitter, adding the settings to a new opts module required in init.lua:
-- ~/.config/nvim/init.lua
require('folding-opts')
-- ~/.config/nvim/lua/folding-opts.lua
vim.wo.foldmethod = 'expr'
vim.wo.foldexpr = 'nvim_treesitter#foldexpr()'
After finishing installing and configuring the parser, the next task on my list was to set up project search functionality:
Fuzzy finder
To allow fuzzy finding files, buffers and more when working on Rust projects I installed telescope.nvim:
-- ~/.config/nvim/lua/plugins/specs.lua
return {
-- snip
{ "nvim-lua/plenary.nvim" },
{ "nvim-telescope/telescope.nvim" },
{ "nvim-telescope/telescope-fzf-native.nvim", build = "make" },
}
plenary.nvim is a requirement for telescope, and telescope-fzf-native.nvim is a recommended optional dependency that improves search speed.
Keymaps were configured in a new setup module, again required from the plugins module:
-- ~/.config/nvim/lua/plugins.lua
require('plugins.setup.telescope')
-- ~/.config/nvim/lua/plugins/setup/telescope.lua
local builtin = require('telescope.builtin')
vim.keymap.set('n', '<leader>ff', builtin.find_files, {})
vim.keymap.set('n', '<leader>fg', builtin.live_grep, {})
vim.keymap.set('n', '<leader>fb', builtin.buffers, {})
vim.keymap.set('n', '<leader>fh', builtin.help_tags, {})
Next task was to get a terminal working inside Neovim, so I didn’t need to leave it to run Cargo or Git commands:
Terminal
I installed vim-floaterm to allow me to bring up a floating terminal window inside Neovim:
-- ~/.config/nvim/lua/plugins/specs.lua
return {
-- snip
{ "voldikss/vim-floaterm" },
}
And configured keymaps for it in a new opts module, required from init.lua:
-- ~/.config/nvim/init.lua
require('terminal-opts')
-- ~/.config/nvim/lua/terminal-opts.lua
vim.cmd([[
nnoremap <Leader>ft :FloatermNew --name=myfloat --height=0.8 --width=0.7 --autoclose=2 fish <CR>
nnoremap t :FloatermToggle myfloat<CR>
]])
vim.keymap.set('t', "<Esc>", "<C-\\><C-n>:q<CR>")
The last plugin I wanted to install was one to allow EasyMotion style buffer navigation:
EasyMotion style navigation
To enable quick navigation around the code in a buffer I installed hop.nvim:
-- ~/.config/nvim/lua/plugins/specs.lua
return {
-- snip
{ "smoka7/hop.nvim" },
}
And configured keymaps for it:
-- ~/.config/nvim/lua/plugins.lua
require('plugins.setup.hop')
-- ~/.config/nvim/lua/plugins/setup/hop.lua
local hop = require('hop')
hop.setup()
vim.keymap.set('n', '<Leader>hw', hop.hint_words)
vim.keymap.set('n', '<Leader>hc', hop.hint_char2)
vim.keymap.set('n', '<Leader>hl', hop.hint_lines)
vim.keymap.set('n', '<Leader>hp', hop.hint_patterns)