DEV Community

Cover image for Rust and Neovim - A Thorough Guide and Walkthrough
Rodrigo Santiago
Rodrigo Santiago

Posted on • Originally published at rsdlt.github.io

Rust and Neovim - A Thorough Guide and Walkthrough

This post is a detailed explanation and walkthrough of how I set up my Rust development environment and workflow with Neovim.

Prerquisites

  • Have the following installed:

  • Have basic knowledge about Neovim and Lua:

    • How to navigate with the keyboard.
    • How to install / remove plugins.
    • How to create and edit Lua config files.
    • Here is a great tutorial that helped me with these basics.

Why Neovim

Like many developers, I went through a phase of looking for the ideal development set of tools that would be convenient and enjoyable, but most of all, that would boost my productivity.

After extensively trying many text editors and IDEs, I have decided to give Neovim a serious shot and invest time and effort in learning and customizing it heavily.

I have to say, it was no easy feat... But it is also a great experience!

Neovim is a very sophisticated tool that is extremely customizable but carries a steep learning curve. However, once you're past the basics and force yourself some muscle memory it is unbelievably fast. All the effort pays off massively.

I am, by no means, an expert user of Neovim, but with every keystroke I get faster and more productive.

And I think that is the key thing that makes Neovim a different beast: it forces you to change and adapt, and you actually see and feel yourself getting better and better!

Anyways, here are my Pros and Cons about Neovim...

Pros:

  • Lighting fast.
  • Extremely configurable and customizable.
  • Massive ecosystem of plugins.
  • Cross-platform support.
  • Support for almost any programming language out there.
  • Forces productive habits upon you.
  • Abundant documentation and community support.
  • It is free and open source.

Cons:

  • Significant up-front time investment in learning.
  • Abundance of choice in customization options can be distracting.
  • Steep learning curve.
  • Demands commitment and patience in creating new muscle memory.

Why Neovim for Rust Development

Okay, the above points are all valid from a general point of view, but why is Neovim great specifically for a Rust development workflow? Here are my reasons:

  • Neovim natively supports LSP in versions 0.5+
  • rust-analyzer is supported through Neovim's LSP, which essentially give us:
    • Code completion.
    • Some refactoring.
    • Code analysis and linting.
    • Go to definition.
    • Code actions.
    • Access to documentation.
    • Show and go to references.
    • Snippets support.
    • Better syntax highlighting.
    • Code formatting.
  • rust-tools is a fantastic plugin to setup rust-analyzer with Neovim's LSP .
  • Integrated terminal.
  • Access to Cargo commands.
  • Rustfmt On Save.
  • Rust debugging via:
  • TOML language support.

In essence, we have all the functionality that is needed from an IDE to develop Rust with productivity.

Here are a couple screenshots using my current setup:

Rust development with Neovim and Vimspector
Rust development with Neovim and Vimspector

Rust development with Neovim and [voldikss/vim-floaterm] float terminal plugin
Rust development with Neovim and voldikss/vim-floaterm float terminal plugin

Set Up Rust with Neovim

To make things more digestible, here the major activities that what we are going to do:

Rust and Neovim Set Up Diagram - made with https://mermaid.live
Rust and Neovim Set Up Diagram - made with https://mermaid.live

  1. Download and setup rust-analyzer and codelldb using Neovim's plugins.
  2. Attach Neovim to rust-analyzer.
  3. Install Neovim's complete and snippets plugins.
  4. Install Neovim's tree-sitter and set it up with tree-sitter-rust parser.
  5. Set up vimspector to work with codelldb to debug Rust and Rust Tests.

But before we go into the details, here is my Neovim .config/nvim folder so that the filenames below make sense:

.config/nvim on  main [!] via 🌙 v5.4.4
 λ tree -L 2
.
├── init.lua
├── lua
│   ├── keys.lua
│   ├── opts.lua
│   ├── plug.lua
│   └── vars.lua
└── plugin
    └── packer_compiled.lua 
Enter fullscreen mode Exit fullscreen mode

1. Download rust-analyzer and CodeLLDB with Neovim's plugins

The first Neovim plugin, other than Packer (the package manager), we need is Mason:

Here is the Lua code to install these -and other- plugins using Packer:

.config/nvim/lua/plug.lua:

return require('packer').startup(function()
    -- other plugins...

    use 'williamboman/mason.nvim'    
    use 'williamboman/mason-lspconfig.nvim'

   -- other plugins...
end)
Enter fullscreen mode Exit fullscreen mode

Then we just save :w, reload :luafile % and install with :PackerInstall.

I could use other package managers to install plugins or LSPs, however, I prefer Packer and Mason because they are written entirely in Lua, are getting traction within the Neovim developer community, and they work great.

Once Mason is installed, we use it to install and manage rust-analyzer and CodeLLDB:

On Neovim command mode we input :MasonInstall rust-analyzer codelldb.

When Manson finishes, we we can check with :Mason that we have rust-analyzer and CodeLLDB installed.

Mason installing rust-analyzer and CodeLLDB for Neovim
Mason installing rust-analyzer and CodeLLDB for Neovim

2. Attaching Neovim to rust-analyzer

Next, we continue by installing two essential plugins to attach rust-analyzer to Neovim's LSP:

Here is the Lua code to install these packages using Packer:

.config/nvim/lua/plug.lua:

return require('packer').startup(function()
    -- other plugins...

    use 'neovim/nvim-lspconfig' 
    use 'simrat39/rust-tools.nvim'

    -- other plugins...
end)
Enter fullscreen mode Exit fullscreen mode

Now, we need to setup Neovim so that it can interact with rust-analyzer:

.config/nvim/init.lua

local rt = {
    server = {
        settings = {
            on_attach = function(_, bufnr)
                -- Hover actions
                vim.keymap.set("n", "<C-space>", rt.hover_actions.hover_actions, { buffer = bufnr })
                -- Code action groups
                vim.keymap.set("n", "<Leader>a", rt.code_action_group.code_action_group, { buffer = bufnr })
                require 'illuminate'.on_attach(client)
            end,
            ["rust-analyzer"] = {
                checkOnSave = {
                    command = "clippy"
                }, 
            },
        }
    },
}
require('rust-tools').setup(rt)
Enter fullscreen mode Exit fullscreen mode

Further customization options are available via rust-tools configuration.

However, I prefer to leave the defaults in rust-tools, and configure instead through Neovim's Diagnostics API:

.config/nvim/init.lua

-- LSP Diagnostics Options Setup 
local sign = function(opts)
  vim.fn.sign_define(opts.name, {
    texthl = opts.name,
    text = opts.text,
    numhl = ''
  })
end

sign({name = 'DiagnosticSignError', text = ''})
sign({name = 'DiagnosticSignWarn', text = ''})
sign({name = 'DiagnosticSignHint', text = ''})
sign({name = 'DiagnosticSignInfo', text = ''})

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 })
]])
Enter fullscreen mode Exit fullscreen mode

3. Install the Complete and Snippets plugins' suite:

The following set of plugins allow us to query Neovim's LSPs, and other sources, to present the auto-complete drop-down menu while typing code.

Thanks to these plugins, and because we have already attached rust-analyzer to Neovim's LSP, we will be able to get nice IDE-like auto-completion when we work on a Cargo project:

.config/nvim/lua/plug.lua:

return require('packer').startup(function()
    -- other plugins...

    -- Completion framework:
    use 'hrsh7th/nvim-cmp' 

    -- LSP completion source:
    use 'hrsh7th/cmp-nvim-lsp'

    -- Useful completion sources:
    use 'hrsh7th/cmp-nvim-lua'
    use 'hrsh7th/cmp-nvim-lsp-signature-help'
    use 'hrsh7th/cmp-vsnip'                             
    use 'hrsh7th/cmp-path'                              
    use 'hrsh7th/cmp-buffer'                            
    use 'hrsh7th/vim-vsnip'                             

    -- other plugins...
end)
Enter fullscreen mode Exit fullscreen mode

And now, we need to set them up in our configuration files:

First in our Neovim APIs:

.config/nvim/lua/opts.lua


--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 select, force to select one from the menu
-- shortness: avoid showing extra messages when using completion
-- updatetime: set updatetime for CursorHold
vim.opt.completeopt = {'menuone', 'noselect', 'noinsert'}
vim.opt.shortmess = vim.opt.shortmess + { c = true}
vim.api.nvim_set_option('updatetime', 300) 

-- Fixed column for diagnostics to appear
-- Show autodiagnostic popup on cursor hover_range
-- Goto previous / next diagnostic warning / error 
-- Show inlay_hints more frequently 
vim.cmd([[
set signcolumn=yes
autocmd CursorHold * lua vim.diagnostic.open_float(nil, { focusable = false })
]])

Enter fullscreen mode Exit fullscreen mode

And then in our completion plugins:

.config/nvim/init.lua

-- Completion Plugin Setup
local cmp = require'cmp'
cmp.setup({
  -- Enable LSP snippets
  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-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,
    })
  },
  -- Installed sources:
  sources = {
    { name = 'path' },                              -- file paths
    { name = 'nvim_lsp', keyword_length = 3 },      -- from language server
    { name = 'nvim_lsp_signature_help'},            -- display function signatures with current parameter emphasized
    { name = 'nvim_lua', keyword_length = 2},       -- complete neovim's Lua runtime API such vim.lsp.*
    { name = 'buffer', keyword_length = 2 },        -- source current buffer
    { name = 'vsnip', keyword_length = 2 },         -- nvim-cmp source for vim-vsnip 
    { name = 'calc'},                               -- source for math calculation
  },
  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,
  },
})
Enter fullscreen mode Exit fullscreen mode

With this setup we are now able to have real-time completion capabilities sourcing from rust-analyzer and we can review the associated documentation that pops up:

Rust real-time completion in Neovim
Rust real-time completion in Neovim

4. Install Tree-sitter and set it up with the tree-sitter-rust parser

Tree-sitter is a fantastic parser generation and incremental parsing library, that supports Rust language bindings and has an available parser Rust tree-sitter-rust.

Neovim's support of Tree-sitter is, at the time of this post, experimental and should be treated as such; however, I have found no issues so far with my setup.

First we need to insall the nvim-treesitter/nvim-treesitter plugin:

.config/nvim/lua/plug.lua:

return require('packer').startup(function()
    -- other plugins...

    use 'nvim-treesitter/nvim-treesitter'

    -- other plugins...
end)
Enter fullscreen mode Exit fullscreen mode

And then configure it... .config/nvim/init.lua

-- Treesitter Plugin Setup 
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,
  }
}

Enter fullscreen mode Exit fullscreen mode

We need to be sure that the ensure_installed = {... "rust", "toml" ...} variable is part of the setup function and that "rust" and "toml" languages are included...

nvim-treesitter allows us to install the tree-sitter-rust parser, effectively enabling all the benefits of Tree-sitter for Rust in Neovim!

We can also check the status of our parsers with :TSInstallInfo and update them with :TSUpdate on the command prompt.

nvim-treesitter also provides a nice view of our parsers with the :checkhealth command and the specific support (e.g. Highlights, Folds, etc.) for each language:

nvim-treesitter: require("nvim-treesitter.health").check()
========================================================================
## Installation
  - WARNING: `tree-sitter` executable not found (parser generator, only needed for :TSInstallFromGrammar, not required for :TSInstall)
  - OK: `node` found v17.7.1 (only needed for :TSInstallFromGrammar)
  - OK: `git` executable found.
  - OK: `cc` executable found. Selected from { vim.NIL, "cc", "gcc", "clang", "cl", "zig" }
    Version: cc (SUSE Linux) 12.1.1 20220721 [revision 4f15d2234608e82159d030dadb17af678cfad626]
  - OK: Neovim was compiled with tree-sitter runtime ABI version 14 (required >=13). Parsers must be compatible with runtime ABI.

## Parser/Features H L F I J
  - toml           ✓ ✓ ✓ ✓ ✓
  - lua            ✓ ✓ ✓ ✓ ✓
  - rust           ✓ ✓ ✓ ✓ ✓

  Legend: H[ighlight], L[ocals], F[olds], I[ndents], In[j]ections
         +) multiple parsers found, only one will be used
         x) errors found in the query, try to run :TSUpdate {lang}
Enter fullscreen mode Exit fullscreen mode

And we can customize as we please.

In my case, I am connecting the folding API in Neovim to the Tree-sitters folding functions:

.config/nvim/lua/opts.lua

-- Treesitter folding 
vim.wo.foldmethod = 'expr'
vim.wo.foldexpr = 'nvim_treesitter#foldexpr()'
Enter fullscreen mode Exit fullscreen mode

And here is the result of my Rust code being folded in Neovim using Tree-sitter:

Rust code folding in Neovim using Tree-sitter
Rust code folding in Neovim using Tree-sitter

5. Set up Vimspector and CodeLLDB to debug Rust and Rust Tests

Okay, now comes one of the most impotart features of any IDE: having a debugger a couple of clicks away to troubleshoot your code.

As of today, my prefered method is to use the Vimspector plugin with CodeLLDB.

There is also the alternative to use nvim-DAP instead of Vimspector. It looks like a fantastic initiative that is getting traction, but I still have not enabled it in my workflow. Definitely worth a look!

Remember that we already installed CodeLLDB via Mason, so now we just need to install the Vimspector plugin:

.config/nvim/lua/plug.lua:

return require('packer').startup(function()
    -- other plugins...

    use 'puremourning/vimspector'

    -- other plugins...
end)
Enter fullscreen mode Exit fullscreen mode

Second, we can configure Vimspector UI and keymap options to our liking:

.config/nvim/lua/opts.lua:

-- Vimspector options
vim.cmd([[
let g:vimspector_sidebar_width = 85
let g:vimspector_bottombar_height = 15
let g:vimspector_terminal_maxwidth = 70
]])
Enter fullscreen mode Exit fullscreen mode

.config/nvim/lua/keys.lua

-- Vimspector
vim.cmd([[
nmap <F9> <cmd>call vimspector#Launch()<cr>
nmap <F5> <cmd>call vimspector#StepOver()<cr>
nmap <F8> <cmd>call vimspector#Reset()<cr>
nmap <F11> <cmd>call vimspector#StepOver()<cr>")
nmap <F12> <cmd>call vimspector#StepOut()<cr>")
nmap <F10> <cmd>call vimspector#StepInto()<cr>")
]])
map('n', "Db", ":call vimspector#ToggleBreakpoint()<cr>")
map('n', "Dw", ":call vimspector#AddWatch()<cr>")
map('n', "De", ":call vimspector#Evaluate()<cr>")

Enter fullscreen mode Exit fullscreen mode

And finally, the tricky part:

  1. Compile our Rust project with cargo build, cargo run, cargo test, so that we can produce a binary.
  2. Create a .vimspector.json file at the root of your Cargo directory.
  3. Attach CodeLLDB and your Rust application binary to Vimspector in the .vimspector.json file.
  4. Create a debugger breakpoint in your code with :call vimspector#ToggleBreakpoint().
  5. Launch Vimspector and start debugging with :call vimspector#Launch().

app/.vimspector.json

{
  "configurations": {
    "launch": {
      "adapter": "CodeLLDB",
      "filetypes": [ "rust" ],
      "configuration": {
        "request": "launch",
        "program": "${workspaceRoot}/target/debug/app"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

And, if everything went well we should be greeted by a new debugging session of our Rust code in Neovim.

Rust debugging in Neovim with Vimspector and CodeLLDB
Rust debugging in Neovim with Vimspector and CodeLLDB

If we want to debug Rust Tests we need to:

  1. Compile the test with cargo test.
  2. Locate the binary produced and printed by the cargo test command (target/debug/deps/app-0683da2c6affeec0 in the example below).
  3. Update .vimspector.json to attach the test binary to the debugging session.
  4. Set a breakpoint inside our test scope and launch Vimspector.
 λ cargo test
   Compiling app v0.1.0 (/home/rsdlt/Documents/RustProjects/app)
    Finished test [unoptimized + debuginfo] target(s) in 0.26s
     Running unittests src/main.rs (target/debug/deps/app-0683da2c6affeec0)

running 2 tests
test tests::first_test ... ok
test tests::test_persona ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Enter fullscreen mode Exit fullscreen mode

app/.vimspector.json

{
  "configurations": {
    "launch": {
      "adapter": "CodeLLDB",
      "filetypes": [ "rust" ],
      "configuration": {
        "request": "launch",
        "program": "${workspaceRoot}/target/debug/deps/app-0683da2c6affeec0"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

And again, if everything is properly configured we are greeted with a debugging session of a Rust Test:

Debugging a Rust Test in Neovim with Vimspector and CodeLLDB
Debugging a Rust Test in Neovim with Vimspector and CodeLLDB

Phew, the hard part is over...

Cargo Power with Terminal Access

One of my favorite plugins in Neovim is voldikss/vim-floaterm. I just love the capability to hit a keystroke and have a terminal prompt pop-up right in front of me to input a fast command and then return to my code just as quickly...

In Rust development I mainly use the terminal to execute Cargo commands like cargo run, cargo build, cargo check and cargo test, among others.

To install this plugin:

.config/nvim/lua/plug.lua:

return require('packer').startup(function()
    -- other plugins...

    use 'voldikss/vim-floaterm'

    -- other plugins...
end)
Enter fullscreen mode Exit fullscreen mode

And I have it configured so that I can toggle the terminal pop-up by just pressing t after creating a session with <leader>ft:

-- FloaTerm configuration
map('n', "<leader>ft", ":FloatermNew --name=myfloat --height=0.8 --width=0.7 --autoclose=2 fish <CR> ")
map('n', "t", ":FloatermToggle myfloat<CR>")
map('t', "<Esc>", "<C-\\><C-n>:q<CR>")
Enter fullscreen mode Exit fullscreen mode

Searching at the Speed of Rust

Rust is fast, and so should be getting anywhere in our code.

There are countless plugins and solutions to find and jump within a project. However, in my experience these two plugins are the most essential:

Telescope can leverage other plugins like BurntSushi/ripgrep and sharkdp/fd

Hop is unbelievably joyful, fun and effective to use.

Project status

I mainly use four plugins to keep my project nice and tidy:

And of course, with just one keystroke we can toggle on / off each of these features really fast...

Rust in Neovim with File Explorer, Tagbar, TODOs and Trouble
Rust in Neovim with File Explorer, Tagbar, TODOs and Trouble

Better Coding Experience

Other plugins that I use that make my coding experience more enjoyable are:

Nice Look & Feel

And last, but not least, we can make Neovim look awesome with just a few plugins and tweaks:


Links, references and disclaimers:

Here is the full list of all the Neovim plugins I use as of the date of this post. The credit, and my gratitude, goes to all the hardworking developers that make these awesome open source tools:

Neovim Plugins:
(In alphabetical order)


A version of this post was orginally published on https://rsdlt.github.io

Top comments (5)

Collapse
 
phnaharris profile image
phnaharris

Thank you for your content. I'm using Rust with nvim recently for my development workflow. I see that when I open rust file that import a large codebase (solana program library) in background, rust-analyzer open a new instance for each buffer (background), and cause very high cpu (100% at all 12 threads) and ram (20gb for rust-analyzer). I dont see that problem with vscode (vscode just open an rust-analyzer instance). Did you face with that problem, and can you help me to fix it (or it's nvim-lsp feature???)???

I use vimplug, nvim-lsp (with lsp-saga and nvim-cmp) and rust-tool.

Thank you.

Collapse
 
rsdlt profile image
Rodrigo Santiago

Hi and thanks for reading. With rust-analyzer I have not experienced an issue with CPU load, but I have noticed that it indeed opens a new instance per buffer consuming ~257M of RAM:

rust-analyzer memory

And something similar happens with other LSPs servers. For example, this is the load for Texlab with two buffers open:

texlab memory

However, I rarely have more than 5 or 6 buffers open simultaneously, so it has no impact on my workflow.

I would suggest that you open a report in the Neovim Core LSP repo

Collapse
 
phnaharris profile image
phnaharris

Hello friend. After a month facing with that problem, I've found that why my computer get burned when I open a large imported project. Because I use a plugin called lspsaga.nvim for better UI, and that plugin open a bunch of buffers (for better previewing experience), cause the rust-analyzer start a new process attached to each buffer. That is the reason I think till now. Just updating it's not a bug from nvim lsp config.

Collapse
 
phnaharris profile image
phnaharris

Thank you for your reply. I've just found that my project rely on a large code base from another crate, so that is the reason why that the rust-analyzer take a lot of resource. And ya the lsp start a new instance per buffer, but vscode open just one (according my experience), so that why I have an bad experience with that specific project in nvim.

Collapse
 
afarhidev profile image
Aidan Farhi

Do you find this setup productive in a real-world work setting? I like the idea of using the "most optimal" setup but I am still wondering if it will translate over to my day-to-day engineering work within a large enterprise.

Great article by the way!