Neovim does not include built-in debugging support, but we can add
it using plugins like nvim-dap, which integrates the Debug Adapter Protocol (DAP). DAP is a widely adopted protocol used by many popular debuggers
like gdb, lldb, and Node.js.
require('lazy').setup({
{
'mfussenegger/nvim-dap',
}
})
Once nvim-dap is installed, the next step is to install a debugger
that supports DAP. Since I mainly work with Node.js, I'll set up
debugging for Node.js, but the process is quite similar for other
debuggers such as cppdbg for c++ or delve for golang.
Node.js has built-in support for debugging, but it uses the Chrome DevTools Protocol (CDP) instead of DAP. To bridge the gap between DAP and CDP, we need an additional
layer. This is where vscode-js-debug comes in. It acts as a translator between the two protocols.
For installation, I'll use Mason along with Mason Tool Installer, as i already rely on them to manage my language servers.
mason_tool_installer.setup {
ensure_installed = {
"js-debug-adapter",
},
}
And thats it, we have installed everything we need. The next step is
to configure the debug adapter.
dap.adapters['pwa-node'] = {
id = 'pwa-node',
type = 'server',
host = "localhost",
port = 43229,
executable = {
command = "node",
args = { "/root/.local/share/nvim/mason/packages/js-debug-adapter/js-debug/src/dapDebugServer.js", "43229" }
},
options = {
detached = false
}
}
The type property tells nvim-dap how to connect
to the debugger. If set to "server", it
connects over TCP using the specified host and port. If set to "executable", it runs the debugger as a local process. Setting detached = false
ensures that the debugger process is attached to Neovim and will stop
when Neovim exits. This debug adapter can now be used in the language
specific
launch configuration.
dap.configurations.javascript = {
{
name = "Launch file",
type = "pwa-node",
request = "launch",
program = function()
local currentFilePath = vim.fn.expand("%")
return vim.fn.input('Path to executable: ', currentFilePath, 'file')
end,
cwd = '${workspaceFolder}',
},
{
name = "Attach to process",
type = "pwa-node",
request = "attach",
port = 9229,
restart = true,
cwd = vim.fn.getcwd(),
sourceMaps = true,
protocol = 'inspector',
},
}
In the example above we have added two configurations. One to
actually start the application (Launch file)
and one that can attach to an already running node process (Attach to process). The name property can be chosen freely and
is used to identify the configuration in the UI. The type must match the type value defined in the corresponding adapter configuration.
And thats it. Now we can open any javascript file and run :DabContinue. This will open a vim select where we can choose between the two
configurations.
The final step is to set up some convenient key mappings, so you
don’t have to type out the commands every time. These are the
mappings I use, feel free to copy or modify them to fit your
workflow.
Mapping
Description
Command
<leader>dc
[d]ebug [c]ontinue
:DapContinue
<leader>db
[d]ebug [t]oggle breakpoint
:DapToggleBreakpoint
<leader>dn
[d]ebug [n]ext (step over)
:DapStepOver
<leader>di
[d]ebug [i]nto (step into)
:DapStepInto
<leader>do
[d]ebug [o]ut (step out)
:DapStepOut
Tip
Personally, I’m not a fan of the default icons used by nvim-dap to
indicate breakpoints and the current execution point. So, I use
the following code to assign more visually appealing ones:
vim.fn.sign_define("DapBreakpoint", { ((text = 'B'), (texthl = 'DapBreakpoint'), (linehl = ''), (numhl = '')) })
vim.fn.sign_define("DapStopped", { ((text = '→'), (texthl = 'DapStopped'), (linehl = ''), (numhl = '')) })
vim.fn.sign_define("DapBreakpointRejected", { ((text = 'R'), (texthl = 'DapBreakpointRejected'), (linehl = ''), (numhl = '')) })
vim.cmd [[
highlight DapBreakpoint guifg=#e74856 gui=bold
highlight DapStopped guifg=#328aed gui=bold
highlight DapBreakpointRejected guifg=#ff966c gui=bold
]]