Setting up Neovim for Rust and Go development
UPDATE November 2022
Not long after I wrote this post I had switched from VimScript to using Lua and also making large sets of changes and tweaks to my configuration. The source of truth is:
https://github.com/integralist/nvim (which is a submodule within https://github.com/integralist/dotfiles)
This post is being kept for posterity, but ultimately I would recommend you look at the above dotfiles repo instead.
This is going to be a very focused post because when you’re looking to get your code editor configured you typically just want answers. Let’s go…
NOTE: I configure Neovim with
~/.config/nvim/init.vim
.
Requirements
- Executables.
- Plugin manager.
- An LSP (Language Server Protocol) client.
Executables
There’s a bunch of Rust and Go based tools we’ll want to have installed first.
Add the following to your .bashrc
:
export PATH="/usr/local/go/bin:$PATH"
export PATH="$HOME/go/bin:$PATH"
export PATH="$HOME/.cargo/bin:$PATH"
# rustup
#
# avoid https://github.com/rust-analyzer/rust-analyzer/issues/4172
#
# NOTE: Has to be defined after PATH update to locate .cargo directory.
#
export RUST_SRC_PATH="$(rustc --print sysroot)/lib/rustlib/src/rust/src"
# To support the configuring our go environment we will override the cd
# command to call the go logic for checking the go version.
#
# We also make sure to call ls when changing directories as it's nice to see
# what's in each directory.
#
# NOTE: We use `command` and not `builtin` because the latter doesn't take into
# account anything available on the user's $PATH but also because it didn't
# work with the Starship prompt which seems to override cd also.
function cd {
command cd "$@"
RET=$?
ls
go_version
return $RET
}
# configure go environment
#
# Custom go binaries are installed in $HOME/go/bin.
#
function go_version {
if [ -f "go.mod" ]; then
v=$(grep -E '^go \d.+$' ./go.mod | grep -oE '\d.+$')
if [[ ! $(go version | grep "go$v") ]]; then
echo ""
echo "About to switch go version to: $v"
if ! command -v "$HOME/go/bin/go$v" &> /dev/null
then
echo "run: go install golang.org/dl/go$v@latest && go$v download && sudo cp \$(which go$v) \$(which go)"
return
fi
sudo cp $(which go$v) $(which go)
fi
fi
}
if [ ! -f "$HOME/go/bin/gofumpt" ]; then
go install mvdan.cc/gofumpt@latest
fi
if [ ! -f "$HOME/go/bin/revive" ]; then
go install github.com/mgechev/revive@latest
fi
# configure rust environment
#
# - autocomplete
# - rust-analyzer
# - cargo audit
# - cargo-nextest
# - cargo fmt
# - cargo clippy
# - cargo edit
#
source $HOME/.cargo/env
if [ ! -f "$HOME/.config/rustlang/autocomplete/rustup" ]; then
mkdir -p ~/.config/rustlang/autocomplete
rustup completions bash rustup >> ~/.config/rustlang/autocomplete/rustup
fi
source "$HOME/.config/rustlang/autocomplete/rustup"
if ! command -v rust-analyzer &> /dev/null
then
brew install rust-analyzer
fi
if ! cargo audit --version &> /dev/null; then
cargo install cargo-audit --features=fix
fi
if ! cargo nextest --version &> /dev/null; then
cargo install cargo-nextest
fi
if ! cargo fmt --version &> /dev/null; then
rustup component add rustfmt
fi
if ! cargo clippy --version &> /dev/null; then
rustup component add clippy
fi
if ! ls ~/.cargo/bin | grep 'cargo-upgrade' &> /dev/null; then
cargo install cargo-edit
fi
Plugin manager
I use vim-plug
:
call plug#begin()
Plug 'folke/trouble.nvim'
Plug 'hrsh7th/cmp-buffer'
Plug 'hrsh7th/cmp-nvim-lsp'
Plug 'hrsh7th/cmp-nvim-lsp-signature-help'
Plug 'hrsh7th/cmp-path'
Plug 'hrsh7th/cmp-vsnip'
Plug 'hrsh7th/nvim-cmp'
Plug 'hrsh7th/vim-vsnip'
Plug 'hrsh7th/vim-vsnip-integ'
Plug 'j-hui/fidget.nvim'
Plug 'kosayoda/nvim-lightbulb'
Plug 'm-demare/hlargs.nvim'
Plug 'neovim/nvim-lspconfig'
Plug 'nvim-treesitter/nvim-treesitter', {'do': ':TSUpdate'}
Plug 'simrat39/rust-tools.nvim'
Plug 'weilbith/nvim-code-action-menu'
Plug 'williamboman/nvim-lsp-installer'
call plug#end()
Define the above and run :PlugInstall
. Once everything is installed, continue to the next section.
LSP
I use Neovim’s built-in LSP.
Add the following plugin configuration:
" ------------------------------------
" j-hui/fidget.nvim
" ------------------------------------
"
lua require("fidget").setup()
" ------------------------------------
" kosayoda/nvim-lightbulb
" ------------------------------------
"
autocmd CursorHold,CursorHoldI * lua require('nvim-lightbulb').update_lightbulb()
" ------------------------------------
" weilbith/nvim-code-action-menu
" ------------------------------------
"
let g:code_action_menu_window_border = 'single'
" ------------------------------------
" folke/trouble.nvim
" ------------------------------------
"
lua require("trouble").setup()
" ------------------------------------
" Neovim LSP
" ------------------------------------
"
" Configure Rust LSP.
"
" https://github.com/simrat39/rust-tools.nvim#configuration
"
lua <<EOF
local opts = {
-- rust-tools options
tools = {
autoSetHints = true,
hover_with_actions = true,
inlay_hints = {
show_parameter_hints = true,
parameter_hints_prefix = "",
other_hints_prefix = "",
},
},
-- all the opts to send to nvim-lspconfig
-- these override the defaults set by rust-tools.nvim
-- https://github.com/rust-analyzer/rust-analyzer/blob/master/docs/user/generated_config.adoc
-- https://rust-analyzer.github.io/manual.html#features
server = {
settings = {
["rust-analyzer"] = {
assist = {
importEnforceGranularity = true,
importPrefix = "crate"
},
cargo = {
allFeatures = true
},
checkOnSave = {
-- default: `cargo check`
command = "clippy"
},
},
inlayHints = {
lifetimeElisionHints = {
enable = true,
useParameterNames = true
},
},
}
},
}
require('rust-tools').setup(opts)
EOF
" Configure Golang LSP.
"
" https://github.com/golang/tools/blob/master/gopls/doc/settings.md
" https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md
" https://github.com/golang/tools/blob/master/gopls/doc/vim.md#neovim
" https://github.com/neovim/nvim-lspconfig/blob/master/doc/server_configurations.md#gopls
" https://github.com/golang/tools/blob/master/gopls/doc/vim.md#neovim
" https://www.getman.io/posts/programming-go-in-neovim/
"
lua <<EOF
require('lspconfig').gopls.setup{
cmd = {'gopls'},
settings = {
gopls = {
analyses = {
nilness = true,
unusedparams = true,
unusedwrite = true,
useany = true,
},
experimentalPostfixCompletions = true,
gofumpt = true,
staticcheck = true,
usePlaceholders = true,
},
},
on_attach = on_attach,
}
EOF
" Configure Golang Environment.
"
fun! GoFumpt()
:silent !gofumpt -w %
:edit
endfun
autocmd FileType go map <buffer> <leader>p :call append(".", "fmt.Printf(\"%+v\\n\", )")<CR> <bar> :norm $a<CR><esc>j==$i
autocmd FileType go map <buffer> <leader>e :call append(".", "if err != nil {return err}")<CR> <bar> :w<CR>
autocmd BufWritePost *.go call GoFumpt()
autocmd BufWritePost *.go :cex system('revive '..expand('%:p')) | cwindow
" Order imports on save, like goimports does:
"
lua <<EOF
function OrgImports(wait_ms)
local params = vim.lsp.util.make_range_params()
params.context = {only = {"source.organizeImports"}}
local result = vim.lsp.buf_request_sync(0, "textDocument/codeAction", params, wait_ms)
for _, res in pairs(result or {}) do
for _, r in pairs(res.result or {}) do
if r.edit then
vim.lsp.util.apply_workspace_edit(r.edit, "UTF-8")
else
vim.lsp.buf.execute_command(r.command)
end
end
end
end
EOF
autocmd BufWritePre *.go lua OrgImports(1000)
" Configure LSP code navigation shortcuts
" as found in :help lsp
"
nnoremap <silent> <c-]> <cmd>lua vim.lsp.buf.definition()<CR>
nnoremap <silent> <c-k> <cmd>lua vim.lsp.buf.signature_help()<CR>
nnoremap <silent> K <cmd>lua vim.lsp.buf.hover()<CR>
nnoremap <silent> gi <cmd>lua vim.lsp.buf.implementation()<CR>
nnoremap <silent> gc <cmd>lua vim.lsp.buf.incoming_calls()<CR>
nnoremap <silent> gd <cmd>lua vim.lsp.buf.type_definition()<CR>
nnoremap <silent> gr <cmd>lua vim.lsp.buf.references()<CR>
nnoremap <silent> gn <cmd>lua vim.lsp.buf.rename()<CR>
nnoremap <silent> gs <cmd>lua vim.lsp.buf.document_symbol()<CR>
nnoremap <silent> gw <cmd>lua vim.lsp.buf.workspace_symbol()<CR>
" Replaced LSP implementation with code action plugin...
"
" nnoremap <silent> ga <cmd>lua vim.lsp.buf.code_action()<CR>
"
nnoremap <silent> ga <cmd>CodeActionMenu<CR>
nnoremap <silent> [x <cmd>lua vim.diagnostic.goto_prev()<CR>
nnoremap <silent> ]x <cmd>lua vim.diagnostic.goto_next()<CR>
nnoremap <silent> ]s <cmd>lua vim.diagnostic.show()<CR>
" Replaced LSP implementation with trouble plugin...
"
" nnoremap <silent> <space>q <cmd>lua vim.diagnostic.setloclist()<CR>
"
nnoremap <silent> <space>q <cmd>Trouble<CR>
" Setup Completion
" https://github.com/hrsh7th/nvim-cmp#recommended-configuration
"
lua <<EOF
local cmp = require('cmp')
cmp.setup({
snippet = {
expand = function(args)
vim.fn["vsnip#anonymous"](args.body)
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-d>'] = cmp.mapping.scroll_docs(-4),
['<C-f>'] = cmp.mapping.scroll_docs(4),
['<C-Space>'] = cmp.mapping(cmp.mapping.complete(), { 'i', 'c' }),
['<C-e>'] = cmp.mapping.close(),
['<CR>'] = cmp.mapping.confirm({
behavior = cmp.ConfirmBehavior.Insert,
select = true,
})
},
sources = {
{ name = 'nvim_lsp' },
{ name = 'vsnip' },
{ name = 'path' },
{ name = 'buffer' },
{ name = 'nvim_lsp_signature_help' },
},
})
EOF
" Setup Treesitter and friends
"
" NOTE: originally used `ensure_installed = "all"` but an experimental PHP
" parser was causing NPM lockfile errors.
"
lua <<EOF
require('nvim-treesitter.configs').setup {
ensure_installed = { "bash", "c", "cmake", "css", "dockerfile", "go", "gomod", "gowork", "hcl", "help", "html", "http", "javascript", "json", "lua", "make", "markdown", "python", "regex", "ruby", "rust", "toml", "vim", "yaml", "zig" },
highlight = {
enable = true,
},
rainbow = {
enable = true,
extended_mode = true,
max_file_lines = nil,
}
}
require('hlargs').setup()
EOF