I have used many editors in the last 5 years.
Sublime Text, Vim, then CLion, then VSCode, back to Vim, briefly Onivim and now Neovim.
I find it important to experiment with different editors and IDEs in order to have an idea of what powers they hold and how they could be included in your development toolbox.
Over the last couple months, I have been looking at ways to “sharpen” my development toolset and have been playing around with multiple vim configurations, one in which I’d like to share today.
Neovim is a fork of vim, which is focused on extensibility and usability. An example of this is the ability to use Lua instead of VimL for plugins providing greater flexibility for extending the editor.
In the 0.5 release of Neovim, the developers have introduced
an Language Server Protocol
(LSP) client framework (:help lsp
)
This means, Neovim can act as a client to LSP servers (like rust-analyzer) and assist in building enhanced LSP tools.
LSP facilitates programming language specific features such as go-to-definition, completion, refactoring, formatting, etc. The goal of LSP is to separate language support and the editor.
Why use LSP? Well, for one, it allows the developers of an editor to focus on the editor and not of specific language support. This is a win-win for language providers and those who release tooling. This is turning a X*Y problem into X+Y. (Where X is the number of editors and Y is the number of languages). There are LSP servers available for almost every language out there.
So how do we configure Neovim LSP with rust-analyzer? Simple!
Check out this github repository for the complete, up-to date configuration.
https://github.com/sharksforarms/neovim-rust
Let’s start with the prerequisites:
- Neovim >= 0.8, see Installing Neovim
- Currently, 0.8 can be found as a github download, in the unstable PPA or other repositories. I am currently living on the bleeding edge (0.9): building and installing neovim from the master git branch.
- Install rust-analyzer
Note: The binary must be in your environment’s PATH
Diving in, let’s install some plugins.
The plugin manager used here is packer.nvim, but any plugin manager can be used.
Let’s start off with a fresh ~/.config/nvim/init.lua
file.
-- ensure the packer plugin manager is installed
local ensure_packer = function()
local fn = vim.fn
local install_path = fn.stdpath("data") .. "/site/pack/packer/start/packer.nvim"
if fn.empty(fn.glob(install_path)) > 0 then
fn.system({ "git", "clone", "--depth", "1", "https://github.com/wbthomason/packer.nvim", install_path })
vim.cmd([[packadd packer.nvim]])
return true
end
return false
end
local packer_bootstrap = ensure_packer()
require("packer").startup(function(use)
-- Packer can manage itself
use("wbthomason/packer.nvim")
-- Collection of common configurations for the Nvim LSP client
use("neovim/nvim-lspconfig")
-- Visualize lsp progress
use({
"j-hui/fidget.nvim",
config = function()
require("fidget").setup()
end
})
-- Autocompletion framework
use("hrsh7th/nvim-cmp")
use({
-- cmp LSP completion
"hrsh7th/cmp-nvim-lsp",
-- cmp Snippet completion
"hrsh7th/cmp-vsnip",
-- cmp Path completion
"hrsh7th/cmp-path",
"hrsh7th/cmp-buffer",
after = { "hrsh7th/nvim-cmp" },
requires = { "hrsh7th/nvim-cmp" },
})
-- See hrsh7th other plugins for more great completion sources!
-- Snippet engine
use('hrsh7th/vim-vsnip')
-- Adds extra functionality over rust analyzer
use("simrat39/rust-tools.nvim")
-- Optional
use("nvim-lua/popup.nvim")
use("nvim-lua/plenary.nvim")
use("nvim-telescope/telescope.nvim")
-- Some color scheme other then default
use("arcticicestudio/nord-vim")
end)
-- the first run will install packer and our plugins
if packer_bootstrap then
require("packer").sync()
return
end
To install the above run the :PackerUpdate
command in neovim, or start it with nvim +PackerUpdate
.
Let’s setup the rust-analyzer LSP and start configuring the completion
-- Set completeopt to have a better completion experience
-- :help completeopt
-- menuone: popup even when there's only one match
-- noinsert: Do not insert text until a selection is made
-- noselect: Do not auto-select, nvim-cmp plugin will handle this for us.
vim.o.completeopt = "menuone,noinsert,noselect"
-- Avoid showing extra messages when using completion
vim.opt.shortmess = vim.opt.shortmess + "c"
local function on_attach(client, buffer)
-- This callback is called when the LSP is atttached/enabled for this buffer
-- we could set keymaps related to LSP, etc here.
end
-- Configure LSP through rust-tools.nvim plugin.
-- rust-tools will configure and enable certain LSP features for us.
-- See https://github.com/simrat39/rust-tools.nvim#configuration
local opts = {
tools = {
runnables = {
use_telescope = true,
},
inlay_hints = {
auto = true,
show_parameter_hints = false,
parameter_hints_prefix = "",
other_hints_prefix = "",
},
},
-- all the opts to send to nvim-lspconfig
-- these override the defaults set by rust-tools.nvim
-- see https://github.com/neovim/nvim-lspconfig/blob/master/CONFIG.md#rust_analyzer
server = {
-- on_attach is a callback called when the language server attachs to the buffer
on_attach = on_attach,
settings = {
-- to enable rust-analyzer settings visit:
-- https://github.com/rust-analyzer/rust-analyzer/blob/master/docs/user/generated_config.adoc
["rust-analyzer"] = {
-- enable clippy on save
checkOnSave = {
command = "clippy",
},
},
},
},
}
require("rust-tools").setup(opts)
-- Setup Completion
-- See https://github.com/hrsh7th/nvim-cmp#basic-configuration
local cmp = require("cmp")
cmp.setup({
preselect = cmp.PreselectMode.None,
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(),
-- Add tab support
["<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.complete(),
["<C-e>"] = cmp.mapping.close(),
["<CR>"] = cmp.mapping.confirm({
behavior = cmp.ConfirmBehavior.Insert,
select = true,
}),
},
-- Installed sources
sources = {
{ name = "nvim_lsp" },
{ name = "vsnip" },
{ name = "path" },
{ name = "buffer" },
},
})
Now when nvim is restarted, you should be able to autocomplete and view warnings and errors inside the editor!
And inlay hints!
What about key maps and code navigation? (:help lsp
)
This can be added to the on_attach
callback, we only want these keymaps to be
available when the LSP is attached to the buffer.
local keymap_opts = { buffer = buffer }
-- Code navigation and shortcuts
vim.keymap.set("n", "<c-]>", vim.lsp.buf.definition, keymap_opts)
vim.keymap.set("n", "K", vim.lsp.buf.hover, keymap_opts)
vim.keymap.set("n", "gD", vim.lsp.buf.implementation, keymap_opts)
vim.keymap.set("n", "<c-k>", vim.lsp.buf.signature_help, keymap_opts)
vim.keymap.set("n", "1gD", vim.lsp.buf.type_definition, keymap_opts)
vim.keymap.set("n", "gr", vim.lsp.buf.references, keymap_opts)
vim.keymap.set("n", "g0", vim.lsp.buf.document_symbol, keymap_opts)
vim.keymap.set("n", "gW", vim.lsp.buf.workspace_symbol, keymap_opts)
vim.keymap.set("n", "gd", vim.lsp.buf.definition, keymap_opts)
Code actions are also very useful.
vim.keymap.set("n", "ga", vim.lsp.buf.code_action, keymap_opts)
Let’s improve the diagnostics experience. Same with the keymaps, we could add
this to the on_attach
callback.
-- Set updatetime for CursorHold
-- 300ms of no cursor movement to trigger CursorHold
vim.opt.updatetime = 100
-- Show diagnostic popup on cursor hover
local diag_float_grp = vim.api.nvim_create_augroup("DiagnosticFloat", { clear = true })
vim.api.nvim_create_autocmd("CursorHold", {
callback = function()
vim.diagnostic.open_float(nil, { focusable = false })
end,
group = diag_float_grp,
})
-- Goto previous/next diagnostic warning/error
vim.keymap.set("n", "g[", vim.diagnostic.goto_prev, keymap_opts)
vim.keymap.set("n", "g]", vim.diagnostic.goto_next, keymap_opts)
You may notice, there’s a slight vertical jitter when a new diagnostic comes in.
To avoid this, you can set signcolumn
-- have a fixed column for the diagnostics to appear in
-- this removes the jitter when warnings/errors flow in
vim.wo.signcolumn = "yes"
What’s Next? Link to heading
Here’s some other great plugins to keep you going.
Neovim LSP
The built in neovim LSP combined with neovim features can be very powerful.
Here’s an example of “format-on-write” (with a timeout of 200ms)
local format_sync_grp = vim.api.nvim_create_augroup("Format", {})
vim.api.nvim_create_autocmd("BufWritePre", {
pattern = "*.rs",
callback = function()
vim.lsp.buf.format({ timeout_ms = 200 })
end,
group = format_sync_grp,
})
Check out :help lsp
for more information!
Better UI
Fuzzy finding
Debugging
Note: you may need to do the following to make debugging work.
sudo apt install lldb-11
sudo ln -s /usr/bin/lldb-vscode-11 /usr/bin/lldb-vscode
To conclude, this introduces a basic and flexible setup for Rust development. Here’s the best part though, it’s simple to configure more languages servers! This setup allows you, the developer, to add more lsp' (just like we did with rust-analyzer) to have a full featured cross-language experience.
Thanks for reading!
Questions? Found an error? Create an issue on Github!
Edits:
- 2022-11-29: Fix format-on-write
- 2022-10-22: Convert viml to ✨lua✨
- 2022-01-09: Use
vim.diagnostic.open_float
instead ofvim.lsp.diagnostic.show_line_diagnostics
See neovim/neovim#15154 - 2021-10-11: Removed references to nightly and added formatting example
- 2021-09-06: Added “what next” section
- 2021-09-06: Added rust-analyzer config example, enable clippy on save.
- 2021-09-01: Updated completion framework, enhanced LSP with rust-tools.nvim and more!
- 2021-02-02: Added
enabled
toinlay_hints
function call to support more hints - 2020-12-23: Updated tab completion config to reflect latest completion.nvim developments
- 2020-12-17: Updated diagnostics and lsp config to reflect latest neovim developments
- 2020-10-05: Added note about code actions and gif
- 2020-09-23: Added note about
signcolumn