Motivation Link to heading
I recently switched to Neovim after over 20 years of using Vim. (At some point
I might write about why I switched, but not today.) One of the few features I
missed in Vim compared to an IDE was debugging: sure, I could fire up dlv
or
gdb
, but I didn’t have syntax highlighted code, automatic display of local
variables, and likely other helpful info I didn’t even know I was missing. Sadly
I had never heard of https://github.com/puremourning/vimspector which does
provide this for Vim. When I was setting up Neovim I frequently looked at
http://www.lazyvim.org/ and https://astronvim.com/ for help and inspiration,
and there I saw nvim-dap
and decided to give it a try. Getting it working
took me a good few evenings of experimentation and testing, so I decided to
document it as a reference for myself and as a resource for others.
Note: I use https://github.com/folke/lazy.nvim to manage my plugins, so the example configs also use lazy.nvim. As a newcomer to Neovim I am completely unfamiliar with other plugin managers so I haven’t tried to translate the example configs to other plugin managers.
Setting up nvim-dap Link to heading
https://github.com/mfussenegger/nvim-dap is is a Debug Adapter Protocol client implementation for Neovim. nvim-dap allows you to:
- Launch an application to debug
- Attach to running applications and debug them
- Set breakpoints and step through code
- Inspect the state of the application
nvim-dap
does the heavy lifting of interfacing with the DAP server.
Surprisingly:
It does not automatically configure DAP servers for different filetypes, you need to do that yourself (we will later). You will see a helpful error message when you set a breakpoint without configuring a DAP server:
No configuration found for `markdown`. You need to add configs to `dap.configurations.markdown` (See `:h dap-configuration)
It does not configure keybindings or commands to control the debugger, you need to do that yourself (we will in this section).
I must admit I found this confusing at first: I installed it and wondered what does it do? The answer is that it provides an API to build on, and the simplest way to build on the API is to configure keybindings yourself.
Configuration:
- I copied keybindings from LazyVim, reformatted them, and eventually removed many of them when I used the UI (described later).
- All of the debugging plugins are lazy loaded. IMHO lazy-loading is overdone, but because I rarely use a debugger I feel it’s worth lazy-loading these.
{
"mfussenegger/nvim-dap",
lazy = true,
-- Copied from LazyVim/lua/lazyvim/plugins/extras/dap/core.lua and
-- modified.
keys = {
{
"<leader>db",
function() require("dap").toggle_breakpoint() end,
desc = "Toggle Breakpoint"
},
{
"<leader>dc",
function() require("dap").continue() end,
desc = "Continue"
},
{
"<leader>dC",
function() require("dap").run_to_cursor() end,
desc = "Run to Cursor"
},
{
"<leader>dT",
function() require("dap").terminate() end,
desc = "Terminate"
},
},
}
Configuring DAP servers Link to heading
We need to configure a DAP server for each filetype we want to debug. Happily we can use plugins to do this for many filetypes rather than writing the config ourselves.
https://github.com/jay-babu/mason-nvim-dap.nvim configures the majority of DAP
servers for us. Getting this to work was an exercise in frustration, because
one piece of the config (handlers = {}
) looks optional but is actually
required (see comment in the config snippet).
{
"jay-babu/mason-nvim-dap.nvim",
---@type MasonNvimDapSettings
opts = {
-- This line is essential to making automatic installation work
-- :exploding-brain
handlers = {},
automatic_installation = {
-- These will be configured by separate plugins.
exclude = {
"delve",
"python",
},
},
-- DAP servers: Mason will be invoked to install these if necessary.
ensure_installed = {
"bash",
"codelldb",
"php",
"python",
},
},
dependencies = {
"mfussenegger/nvim-dap",
"williamboman/mason.nvim",
},
}
See the next section for language-specific plugins for DAP server configuration.
Language-specific plugins Link to heading
I use three language-specific plugins to give me a better DAP server configuration for the languages I write in most: Go, Python, and Rust. There are other language-specific plugins available, see https://github.com/mfussenegger/nvim-dap/wiki/Extensions#language-specific-extensions
Go Link to heading
https://github.com/leoluz/nvim-dap-go provides options for debugging
individual tests. You need to install https://github.com/go-delve/delve and
have it in your $PATH
. I added a keymapping to debug an individual test, but
I haven’t had an opportunity to try it yet.
{
"leoluz/nvim-dap-go",
config = true,
dependencies = {
"mfussenegger/nvim-dap",
},
keys = {
{
"<leader>dt",
function() require('dap-go').debug_test() end,
desc = "Debug test"
},
},
},
Python Link to heading
https://github.com/mfussenegger/nvim-dap-python provides config for debugging
individual tests. You need to install https://github.com/microsoft/debugpy
and configure nvim-dap-python
with the path to a Python binary that can
import debugpy
. https://github.com/williamboman/mason.nvim will install
debugpy
in a virtualenv
, and the correct path for that installation is
~/.local/share/nvim/mason/packages/debugpy/venv/bin/python
- this ensures that
Python can find the debugpy
package.
I haven’t tested this, but if your Python project already uses a virtualenv I
suggest installing debugpy
there so that all the modules you require are
available in one bundle rather than messing with multiple virtualenv
directories. Before starting Neovim activate the virtualenv so that python
from the virtualenv is first in $PATH
. Configure nvim-dap-python
to use
python
(literally python
, not /path/to/python
) so it picks up python
from the virtualenv and hopefully everything will Just Work. This should work
across multiple projects and multiple virtualenvs without reconfiguration.
nvim-dap-python
To test the python
path you configure nvim-dap-python
with, run:
/path/to/python -m debugpy --version
Unusually (in my limited experience) nvim-dap-python
’s setup()
doesn’t take
an options table as a parameter. Instead it takes an optional path to the
python3
binary, and an optional options table.
{
"mfussenegger/nvim-dap-python",
lazy = true,
config = function()
local python = vim.fn.expand("~/.local/share/nvim/mason/packages/debugpy/venv/bin/python")
require("dap-python").setup(python)
end,
-- Consider the mappings at
-- https://github.com/mfussenegger/nvim-dap-python?tab=readme-ov-file#mappings
dependencies = {
"mfussenegger/nvim-dap",
},
},
Rust Link to heading
https://github.com/mrcjkb/rustaceanvim has lot of features, almost all of
which I haven’t explored. Notably it configures LSP differently (I haven’t
noticed the difference though), so if you’re using
https://github.com/neovim/nvim-lspconfig to configure LSP servers, make sure
you remove rust
from that list. The resulting DAP configuration allows you to
debug an individual test, which I found very useful.
All of the other plugins described in this post are lazy-loaded when a
keymapping activates the UI. Because rustaceanvim
reconfigures LSP I have it
configured to load whenever I edit Rust, and it depends on nvim-dap
so that’s
loaded too, but there’s no real downside to that.
return {
{
-- Automatically sets up LSP, so lsp.lua doesn't include rust.
-- Makes debugging work seamlessly.
"mrcjkb/rustaceanvim",
version = '^5', -- Recommended by module.
ft = "rust",
dependencies = {
"mfussenegger/nvim-dap",
},
},
}
Setting up a UI Link to heading
The configuration thus far gives you the ability to run a debugger, but it’s very awkward to see the debugging information or interact with the debugger. To address this need I’m using two plugins:
https://github.com/theHamsta/nvim-dap-virtual-text uses virtual text to display the value of each local variable beside its declaration. There are screenshots on Github showing how it works and the various options you can configure.
{ "theHamsta/nvim-dap-virtual-text", config = true, dependencies = { "mfussenegger/nvim-dap", }, },
https://github.com/rcarriga/nvim-dap-ui provides a full UI, similar to an IDE. I’ve configured a keymapping to display the UI, and because the UI depends on all the other plugins (except
rustaceanvim
) they will all be loaded. Again, there are screenshots and docs on Github to look at.{ "rcarriga/nvim-dap-ui", config = true, keys = { { "<leader>du", function() require("dapui").toggle({}) end, desc = "Dap UI" }, }, dependencies = { "jay-babu/mason-nvim-dap.nvim", "leoluz/nvim-dap-go", "mfussenegger/nvim-dap-python", "nvim-neotest/nvim-nio", "theHamsta/nvim-dap-virtual-text", }, }
My complete config for reference Link to heading
https://github.com/tobinjt/dotfiles/blob/master/.config/nvim/lua/plugins/debugging.lua
You’ll notice that most of the plugins are nested as dependencies of
nvim-dap-ui
; this doesn’t cause any change in functionality, I just prefer
this structure because it’s clear that the plugins are only used there.
https://github.com/tobinjt/dotfiles/blob/master/.config/nvim/lua/plugins/rust.lua
contains the rustaceanvim
config.
Debugging the debugger Link to heading
In my limited experience the biggest problem getting nvim-dap
working is not
configuring it properly for the current filetype. Here are some things to check
when debugging (assuming you’re using the config I’ve described):
" Load all the plugins and display the UI. Do you see the UI?
:lua require("dapui").open()
" Open Lazy and check whether the expected plugins are loaded?
:Lazy
" Print the configuration for Python. If this is empty, focus on changing the
" configuration of nvim-dap-python until you see entries here.
:lua vim.print(require('dap').configurations.python)
" Print the configuration for the current filetype. If this is empty, there is
" most likely something wrong with the configuration of mason-nvim-dap.nvim.
" Make sure you check the filetype is included in the list of supported debug
" adaptors at
" https://github.com/jay-babu/mason-nvim-dap.nvim/blob/main/lua/mason-nvim-dap/mappings/filetypes.lua
:lua vim.print(require('dap').configurations[vim.bo.filetype])
Using the debugger Link to heading
This is a very brief introduction:
" Load all the plugins and display the UI.
:lua require("dapui").open()
" Navigate to where you want to place a breakpoint, then set the breakpoint
" with:
:lua require("dap").set_breakpoint()
" Start the program and run until the breakpoint is hit. Some filetypes have
" many options for running the program, and in that case a menu will be
" displayed for you to choose from.
:lua require("dap").continue()
" Fingers crossed the debugger has stopped the program at your checkpoint!
" You should see information in many of the UI windows, and values displayed
" beside variables in your source code.
" Navigate to the window named `dap-repl-<NUMBER>`, this is where you will enter
" commands to control the debugger. Enter insert mode and you'll see a `dap> `
" prompt. Enter `.help` to show a list of commands - I haven't found another
" reference for the commands :( Continue debugging like you would in GDB or
" dlv.