Skip to content

Instantly share code, notes, and snippets.

@mwcz
Created July 22, 2025 19:59
Show Gist options
  • Save mwcz/45b882937e66f3ab937cb4a68b5c3bc2 to your computer and use it in GitHub Desktop.
Save mwcz/45b882937e66f3ab937cb4a68b5c3bc2 to your computer and use it in GitHub Desktop.

+++ title = "Debugging Rust with Vim's Termdebug" path = '/2025/vim-rust-termdebug' description = 'My search for a debugger workflow.' date = '2025-07-20' draft = true

[taxonomies] tags = [ 'programming', 'rust', ]

[extra] thumbnail = 'thumb.png' katex_enable = true +++

Debug as a first resort.

In my search for an ideal debugging workflow, that's the prime requirement. Running my programs through a debugger shouldn't be a last resort when I'm at the end of my wits. It should be the first and only way I run the program while I'm working on it. Even when there's not a bug, if only to step through my freshly fallen code to understand it more intimately. Even when there's no breakpoints, just to reinforce the habit of Always Be Debugging.

I've chased that goal for years, with only modest success. The first program is launching a program in a debugger, with whatever args or other customizations you need in the moment, is hard. What's easy is launching the program without a debugger. That's problem one.

Requirement 0 launching via debugger should be as easy, or easier, than any other way.

In my case, with Rust, that means launching the program with a debugger attached needs to be as easy, or easier, than typing cargo run with some args (foreshadowing...).

First, I tried gdb, but encountered so much friction1 while setting breakpoints that it was unlikely to become habit. The setting of breakpoints really needs to be in-editor.

Requirement 1 can set breakpoints within the context of the code I'm working on.

Stock gdb also wasn't great at pretty-printing Rust values, and I hadn't yet discovered the rust-gdb wrapper that's bundled with rustup installs.

Requirement 2 has excellent formatters for complex data types in rust

Next, I walked away from gdb and tried nvim-dap and nvim-dap-ui for quite a while, where breakpoint creation was effortless, but I had issues where the debugger's (codelldb's) pretty-printers for complex values would fall out of sync with the rustc version I was using.

Requirement 3 debugger must be sync'd to rust version.

I also had issues when debugging multithreaded programs without a way (that I could find) to pin to the current thread. Debugging multithreaded programs without this feature quickly becomes incoherent as the debugger steps around to seeminly random lines.

Requirement 4 can pin the debugger to the current thread.

Another issue I ran into while using nvim-dap and other DAP-based debuggers is the perplexing inability to inspect return values. Rust is full of implicit returns, but this problem applies to explicit returns as well. Often I'll be paused within a function, wanting to see what the function returns, yet the return value is not bound to a variable. For example:

fn have_some_pi(n: f64) -> f64 {
    PI * n
}

The have_some_pi function has an implicit return value which I might want to inspect while debugging. Since no variable is bound to its value, the value can't be inspected. Sometimes the expression (PI * n) can be evaluated by the debugger, but my experience has been that expression evaluation in Rust almost never works.

For some debugging hotspots, I've chosen to rewrite code in this form, simply to make debugging the return value possible.

fn have_some_pi(n: f64) -> f64 {
  let ret = PI * n;
  ret
}

Now a breakpoint can be set on line 3 in order to inspect the value, but please don't make me do this. Clippy will yell at me.

Requirement 5 can print return values.

TODO: resume here, adding notes about:

  • Req 6: command line arguments (easy in gdb, hard in nvim-dap and almost anything else)
    • vscode launch.json is nice for sharing with a team, but terrible for frictionless debugging of programs with myriad args
  • Req 7: learned from nvim-dap that being able to debug tests (debuggables) easily is massively helpful
  • Req 8: multiple binaries in a workspace should all be debuggable
  • Req 9: context is important, so whatever you're currently editing should be the default assumption for what you want to debug
  • then go over the 8 (looks like I landed on 8) requirements and how my nvim+termdebug+myplugin+rust-gdb+rustup solves them all.
  • note the features of the plugin
    • all are features of termdebug, cargo, or gdb, woven together
    • build & debug binary
    • build & debug tests
    • direct access to GDB via GDB repl (more powerful than the subset of features available through DAP)
    • disable termdebug's override of K which is more commonly used for hover docs (also , -, and +)
    • swap positions of gdb window and stdout window
    • pin to current thread (scheduler locking)

Footnotes

  1. Setting breakpoints in an editor, within the context of the code you're working with, is so much easier than any of the (many!) options offered by gdb, such as by filename:linenumber, or by function name, or regex matching a function.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment