Skip to main content
  1. Posts/

Neovim Rust debugging

·1319 words·7 mins

Summary #

This is an article on a particular issue with debugging at a specific version of rust-tools. If you want to see how to setup debugging in Neovim for Rust, you might prefer to look at Start debugging in Neovim.

UPDATE #

The rust-tools plugin has been updated and this is now fixed so you shouldn’t need to do any of this. If you did follow this you will have to reset your changes then run a PackerUpdate. So if you get an error in PackerUpdate saying it can’t update rust-tools, chances are your changes are still on your drive: ~/.local/share/nvim/site/pack/packer/start/rust-tools.nvim/lua/rust-tools/dap.lua

I will leave the article below in case you are on an old build for some reason.

My struggles with Neovim Rust debugging #

Using Neovim is a shock to the system sometimes for someone used to Visual Studio (the full-featured one, not VS Code) and JetBrains products. Getting debugging working has been a struggle due to not knowing where to start along with running on a Linux distro that updates on a daily basis. It still seems strange that I’m configuring Neovim to call out to VS Code’s version of LLDB, just because it has better Rust support (according to the rust-tools Neovim plugin).

Anyway, this post will show how I’ve setup debugging with rust-tools and VS Code's codelldb. Technically is Code - OSS, I don’t think that should matter though.

The flawed current scenario #

Just before we start, I want to warn you, this is still not ideal. It’s just about good enough for me for now. I have to run :RustDebuggable 3 times before it starts up and connects. Debugging Neovim plugin errors is way out of my comfort zone. The first error states:

:RustDebuggables

Error executing vim.schedule lua callback: vim/shared.lua:0: t: expected table, got nil
stack traceback:
        [C]: in function 'error'
        vim/shared.lua: in function 'validate'
        vim/shared.lua: in function 'tbl_filter'
        ...ker/start/rust-tools.nvim/lua/rust-tools/debuggables.lua:59: in function 'sanitize_results_for_debugging'
        ...ker/start/rust-tools.nvim/lua/rust-tools/debuggables.lua:75: in function 'fn'
        ...ker/start/rust-tools.nvim/lua/rust-tools/utils/utils.lua:101: in function 'handler'
        /usr/share/nvim/runtime/lua/vim/lsp.lua:1025: in function ''
        vim/_editor.lua: in function <vim/_editor.lua:0

I’m not sure if it’s an issue with my plugin config for rust-tools or if there is another issue with Rust-tools on a rolling Linux release.

The next one is a bit easier to guess at, it looks like the debugger is not ready to be connected to:

:RustDebuggables

Couldn't connect to 127.0.0.1:33397: ECONNREFUSED

On the third time of calling :RustDebuggables, everything starts as required. They say third time lucky for a reason…

If you can cope with that, let’s get it setup.

Install the plugins #

First off is installing everything, I use packer at the moment so this is a view of my plugins.lua file:


require('packer').startup(function()

    -- other non-Rust debugging related plugins removed

    use 'simrat39/rust-tools.nvim'

    -- Debugging
    use 'nvim-lua/plenary.nvim'
    use 'mfussenegger/nvim-dap'
    use({ "rcarriga/nvim-dap-ui", requires = { "mfussenegger/nvim-dap" } })

end)                                             

My Neovim config directory structure #

The following shows a cut down version of my directory structure to just highlight the files I’ve modified for debugging.

~ via  v3.10.4 | slow: 179ms 
12:29:56❯ ls -TR .config/nvim             
.config/nvim
├── init.lua
├── lua
│  ├── keymaps.lua
│  └── plugins.lua
└── plugin
   ├── dapui.lua
   └── rusttools.lua

Rust-tools configuration file #

Next up, I have a rusttools.lua file under my plugins directory, the key here is the codelldb_path and the liblldb_path, which come from the rust-tools github readme

local extension_path = vim.env.HOME .. '/.vscode-oss/extensions/vadimcn.vscode-lldb-1.7.0/' 
-- This version has required a dirty patch to ~/.local/share/nvim/site/pack/packer/start/rust-tools.nvim/lua/rust-tools/dap.lua
local codelldb_path = extension_path .. 'adapter/codelldb'
local liblldb_path = extension_path .. 'lldb/lib/liblldb.so'

-- Rust LSP
local opts = {
  -- rust-tools options
  tools = {
    autoSetHints = true,
    hover_with_actions = true,
    inlay_hints = {
      show_parameter_hints = true,
      parameter_hints_prefix = "",
      other_hints_prefix = "",
    },
  },
  hover_actions = {
			-- the border that is used for the hover window
			-- see vim.api.nvim_open_win()
    border = {
      { "╭", "FloatBorder" },
			{ "─", "FloatBorder" },
			{ "╮", "FloatBorder" },
			{ "│", "FloatBorder" },
			{ "╯", "FloatBorder" },
			{ "─", "FloatBorder" },
			{ "╰", "FloatBorder" },
			{ "│", "FloatBorder" },
    },
    -- whether the hover action window gets automatically focused
    -- default: false
    auto_focus = false,
  },
  -- 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
          },
        },
      }
    },
    dap = {
        adapter = require('rust-tools.dap').get_codelldb_adapter(codelldb_path, liblldb_path)
    },
}
require('rust-tools').setup(opts)

Also, note near the bottom of that lua file, the dap object. This is setting up the DAP using a helper function from the rust-tools plugin, the same helper function we need to modify to make work with the 1.7.0 vscode-lldb debugger.

The dirty patch to Rust-tool’s dap.lua #

Speaking of fixes, let’s make them. You can find the instructions in open issue

Using packer, the file can be found at ~/.local/share/nvim/site/pack/packer/start/rust-tools.nvim/lua/rust-tools/dap.lua


local config = require("rust-tools.config")

local loop = vim.loop               -- <-- NEW

local M = {}

local function get_free_port()      -- <-- NEW
  local server = loop.new_tcp()     -- <-- NEW
  server:bind("127.0.0.1", 0)       -- <-- NEW
  return server:getsockname().port  -- <-- NEW
end                                 -- <-- NEW

---For the heroes who want to use it
---@param codelldb_path string
---@param liblldb_path string
---@param port number to pass to codelldb
function M.get_codelldb_adapter(codelldb_path, liblldb_path, port) -- <-- NEW port arg
  return function(callback, _)
    local stdout = vim.loop.new_pipe(false)
    local stderr = vim.loop.new_pipe(false)
    local handle
    local pid_or_err                             -- <-- DELETE port variable as it's now an arg
    local error_message = ""

    port = port or get_free_port()               -- <-- NEW

    local opts = {
      stdio = { nil, stdout, stderr },
      args = { "--liblldb", liblldb_path, "--port", port },        -- <-- NEW port arg
			detached = true,
    }

    handle, pid_or_err = vim.loop.spawn(codelldb_path, opts, function(code)
      stdout:close()
      stderr:close()
      handle:close()
      if code ~= 0 then
        print("codelldb exited with code", code)
        print("error message", error_message)
      end
    end)

    assert(handle, "Error running codelldb: " .. tostring(pid_or_err))

    stdout:read_start(function(err, chunk)
      assert(not err, err)
      if chunk then                              -- <-- CODE REMOVED
        vim.schedule(function()
          require("dap.repl").append(chunk)
        end)
      end
    end)
    stderr:read_start(function(_, chunk)
      if chunk then
        error_message = error_message .. chunk

        vim.schedule(function()
          require("dap.repl").append(chunk)
        end)
      end
    end)
		
    vim.defer_fn(function()                      -- <-- NEW
      vim.schedule(function()                    -- <-- NEW
        callback({                               -- <-- NEW
          type = "server",                       -- <-- NEW
          host = "127.0.0.1",                    -- <-- NEW
          port = port,                           -- <-- NEW
        })                                       -- <-- NEW
      end)                                       -- <-- NEW
    end, 400)                                    -- <-- NEW

  end
end

-- more code but none of it was modified, the next function should be `function M.setup_adapter()`

Editing the file that has been installed by packer obviously means that if you update or sync, packer will override the changes. I did say earlier that this was a dirty patch. You might want to save a copy of dap.lua in case you accidentally update and lose the changes (although I’m hoping the issue gets resolved soon, I might even be tempted to investigate a better way of waiting for the debugger to start and propose it to the maintainers).

With all of this, extra dap-ui config to make debugging a bit prettier and some custom key mappings, I finally have a working debugger for Rust in Neovim without needing to open up CLion.

I hope documenting this saves someone else new to Neovim (probably me when I update packer without thinking) from having to discover it for themselves over a few frustrating hours.

Some extra Neovim diagnostic tricks #

If you have an error appear in the status line and disappear before you get a chance to read it, you can retrieve it with messages:

:messages

If you want to find where Neovim is storing things, use the vim.fn.stdpath:

:lua print(vim.fn.stdpath('data'))

So far I used “cache” and “data” with that stdpath function, it’s how I located the packer install of rust-tools so I could edit the dap.lua file.