A Calustra - Eloy Coto Pereiro Home About Books

Tips for Rust Debugging in Neovim


One of the most important aspects of programming in Rust is efficient debugging. There are various tools available for this purpose; some developers prefer the Debug Adapter Protocol (DAP), which is utilized by default in VSCode. However, for those using Neovim, the trendy direction is nvim-dap.

Since GDB version 1.14, it has been possible to utilize the DAP protocol directly with GDB (using gdb –interpreter dap), allowing clients to consume debugging information. I tried some opt for plugins like dap-ui and the nvim-dap, but I cannot find confort without the GDB REPL, something that I miss in this utilities.

With that said, Vim 8 introduced a powerful tool called Termdebug, which can be configured to emulate the GDB workflow effectively.

In my setup, I have specific constraints:

The first step is to enable Termdebug:

:packadd termdebug

From there, Termdebug can be invoked directly:

:Termdebug binary

Typing the binary path for each interaction can be tedious, so let’s automate it. When running cargo build, there’s an option for JSON output, allowing us to list the created executables:

$ --> cargo build --message-format=json 2> /dev/null | jq -r 'select(.executable !=null) | [.executable]'
$ -->

With this information, we can launch Termdebug directly using a Neovim Lua API:

-- cargo_build_debug Build the cargo crate, and return the executables path
function cargo_build_debug()
    vim.cmd("!cargo build")
    local cargo_output = vim.fn.system("cargo build --message-format=json 2> /dev/null | jq -r 'select(.executable !=null) | [.executable]'")
    return vim.fn.json_decode(cargo_output)

function Debugger()
    vim.cmd('packadd termdebug')
    local filepaths = cargo_build_debug()
    local paths_len = #filepaths
    local path = ""
    if paths_len == 0 then
        error("executable cannot be found")
    elseif paths_len == 1 then
        path = filepaths[1]
        local choice = vim.fn.inputlist(filepaths)
        path = filepaths[choice+1]
    vim.cmd('Termdebug '..path)

vim.api.nvim_create_user_command('DDebug', Debugger, {})

Calling the DDebug function launches a GDB command within Neovim, utilizing the Job Async API, in a separate split where interaction with the GDB REPL is possible. Now, within the code, interactions can be made directly using commands like :Break or :Step.

The commands available in normal mode are:

`:Run` [args]      run the program with [args] or the previous arguments
`:Arguments` {args}  set arguments for the next `:Run`

*:Break*   set a breakpoint at the current line; a sign will be displayed
*:Clear*   delete the breakpoint at the current line

*:Step*    execute the gdb "step" command
*:Over*    execute the gdb "next" command (`:Next` is a Vim command)
*:Until*   execute the gdb "until" command
*:Finish*  execute the gdb "finish" command
*:Continue*    execute the gdb "continue" command
*:Stop*    interrupt the program

Additionally, variables can be inspected:

`:Evaluate`     evaluate the expression under the cursor
`K`         same (see |termdebug_map_K| to disable)
`:Evaluate` {expr}   evaluate {expr}
`:'<,'>Evaluate`     evaluate the Visually selected text

Bonus Point - OpenOCD Debugging

In Rust embedded development, GDB connects to OpenOCD to reach the target server. Typically, a GDB configuration file is created to establish this connection.

In Neovim, we can check if the file exists and, if so, append the configuration and debug directly on the target board:

local stat = vim.loop.fs_stat("openocd.gdb")
if stat then
    path = "-x openocd.gdb " .. path
vim.cmd('Termdebug '..path)

Using this approach enables seamless debugging of your Rust program.

Happy coding!


Related articles: