In the past, I shared a simple nvim configuration that has implemented intelligent prompts, syntax highlighting, code navigation, and other features required for programming. Today, I plan to tidy up the nvim debugging framework dap.
dap is a framework where the client is responsible for displaying various debugging information on nvim, such as showing breakpoints, call stacks, object memory information, etc. The server provides the necessary functionality for the client, and the server is usually a debugger or a debugger wrapper.
This article will use the Mason plugin to install the dap server. I won't elaborate on Mason here, but I will discuss it in detail in the future.
First, let's look at a few images of it working properly:
Running interface
Viewing variable information
Shortcut keys
Function call stack
Installing dap#
Add codelldb to Mason's installation list. Codelldb is the debugging server used by vscode, responsible for providing debugging information to vscode. With this backend, we can easily achieve the same debugging functionality as vscode.
Configuring dap#
Create a new _dap.lua
file in the plugins directory.
return {
"mfussenegger/nvim-dap",
opt = true,
module = { "dap" },
requires = {
{
"theHamsta/nvim-dap-virtual-text",
module = { "nvim-dap-virtual-text" },
},
{
"rcarriga/nvim-dap-ui",
module = { "dapui" },
},
"nvim-telescope/telescope-dap.nvim",
{
"jbyuki/one-small-step-for-vimkind",
module = "osv",
},
},
config = function()
require("config.dap").setup()
end,
disable = false,
}
Some people will use
use
in packer for installation; just changereturn
touse
.
The packer code is already written. Now let's write the config function. In my case, I placed the file in the lua/config/dap/
directory to manage different languages more conveniently.
First, create an init.lua
file in the dap directory. This is the module entry point, and the initialization work starts here.
local M = {}
local function configure()
end
local function configure_exts()
end
local function configure_debuggers()
end
function M.setup()
configure() -- Configuration
configure_exts() -- Extensions
configure_debuggers() -- Debugger
end
configure_debuggers()
return M
In _dap.lua
, require("config.dap").setup()
is called, which refers to the M.setup()
function in config/dap/init.lua
.
Currently, only a shell has been written. Now let's officially configure it.
Shortcut Keys#
When debugging in nvim, the interface is obviously still in the terminal, so we need to use shortcut keys for some operations, such as setting breakpoints, stepping in, stepping out, etc.
Configure the shortcut keys in config/dap/keymaps.lua
.
local M = {}
local whichkey = require "which-key"
-- local legendary = require "legendary"
-- local function keymap(lhs, rhs, desc)
-- vim.keymap.set("n", lhs, rhs, { silent = true, desc = desc })
-- end
function M.setup()
local keymap = {
d = {
name = "DAP",
R = { "<cmd>lua require'dap'.run_to_cursor()<cr>", "Run to Cursor" },
E = { "<cmd>lua require'dapui'.eval(vim.fn.input '[Expression] > ')<cr>", "Evaluate Input" },
C = { "<cmd>lua require'dap'.set_breakpoint(vim.fn.input '[Condition] > ')<cr>", "Conditional Breakpoint" },
U = { "<cmd>lua require'dapui'.toggle()<cr>", "Toggle UI" },
b = { "<cmd>lua require'dap'.step_back()<cr>", "Step Back" },
c = { "<cmd>lua require'dap'.continue()<cr>", "Continue" },
d = { "<cmd>lua require'dap'.disconnect()<cr>", "Disconnect" },
e = { "<cmd>lua require'dapui'.eval()<cr>", "Evaluate" },
g = { "<cmd>lua require'dap'.session()<cr>", "Get Session" },
h = { "<cmd>lua require'dap.ui.widgets'.hover()<cr>", "Hover Variables" },
S = { "<cmd>lua require'dap.ui.widgets'.scopes()<cr>", "Scopes" },
i = { "<cmd>lua require'dap'.step_into()<cr>", "Step Into" },
o = { "<cmd>lua require'dap'.step_over()<cr>", "Step Over" },
p = { "<cmd>lua require'dap'.pause.toggle()<cr>", "Pause" },
q = { "<cmd>lua require'dap'.close()<cr>", "Quit" },
r = { "<cmd>lua require'dap'.repl.toggle()<cr>", "Toggle Repl" },
s = { "<cmd>lua require'dap'.continue()<cr>", "Start" },
t = { "<cmd>lua require'dap'.toggle_breakpoint()<cr>", "Toggle Breakpoint" },
x = { "<cmd>lua require'dap'.terminate()<cr>", "Terminate" },
u = { "<cmd>lua require'dap'.step_out()<cr>", "Step Out" },
},
}
local opts = {
mode = "n",
prefix = "<leader>",
buffer = nil,
silent = true,
noremap = true,
nowait = false,
}
whichkey.register(keymap, opts)
--- require("legendary.integrations.which-key").bind_whichkey(keymap, opts, false)
local keymap_v = {
d = {
name = "Debug",
e = { "<cmd>lua require'dapui'.eval()<cr>", "Evaluate" },
},
}
opts = {
mode = "v",
prefix = "<leader>",
buffer = nil,
silent = true,
noremap = true,
nowait = false,
}
whichkey.register(keymap_v, opts)
--- require("legendary.integrations.which-key").bind_whichkey(keymap_v, opts, false)
end
return M
Here, I have bound the shortcut keys to <leader> d
.
Now, return to init.lua
and call keymaps in the setup
function.
function M.setup()
require("config.dap.keymaps").setup() -- Keymaps
end
dapui#
dapui is a plugin that beautifies the dap interface, which is usually configured by everyone!
local function configure_exts()
require("nvim-dap-virtual-text").setup({
commented = true,
})
local dap, dapui = require("dap"), require("dapui")
dapui.setup({
expand_lines = true,
icons = { expanded = "", collapsed = "", circular = "" },
mappings = {
-- Use a table to apply multiple mappings
expand = { "<CR>", "<2-LeftMouse>" },
open = "o",
remove = "d",
edit = "e",
repl = "r",
toggle = "t",
},
layouts = {
{
elements = {
{ id = "scopes", size = 0.33 },
{ id = "breakpoints", size = 0.17 },
{ id = "stacks", size = 0.25 },
{ id = "watches", size = 0.25 },
},
size = 0.33,
position = "right",
},
{
elements = {
{ id = "repl", size = 0.45 },
{ id = "console", size = 0.55 },
},
size = 0.27,
position = "bottom",
},
},
floating = {
max_height = 0.9,
max_width = 0.5, -- Floats will be treated as percentage of your screen.
border = vim.g.border_chars, -- Border style. Can be 'single', 'double' or 'rounded'
mappings = {
close = { "q", "<Esc>" },
},
},
}) -- use default
dap.listeners.after.event_initialized["dapui_config"] = function()
dapui.open({})
end
dap.listeners.before.event_terminated["dapui_config"] = function()
dapui.close({})
end
dap.listeners.before.event_exited["dapui_config"] = function()
dapui.close({})
end
end
The configuration is generally not much different; perhaps everyone is just copying from one person's configuration.
Configuring Icons#
I also modified a few default icons in the configure function.
local function configure()
local dap_breakpoint = {
breakpoint = {
text = "",
texthl = "LspDiagnosticsSignError",
linehl = "",
numhl = "",
},
rejected = {
text = "",
texthl = "LspDiagnosticsSignHint",
linehl = "",
numhl = "",
},
stopped = {
text = "",
texthl = "LspDiagnosticsSignInformation",
linehl = "DiagnosticUnderlineInfo",
numhl = "LspDiagnosticsSignInformation",
},
}
vim.fn.sign_define("DapBreakpoint", dap_breakpoint.breakpoint)
vim.fn.sign_define("DapStopped", dap_breakpoint.stopped)
vim.fn.sign_define("DapBreakpointRejected", dap_breakpoint.rejected)
end
Breakpoint markers
Step stop
Configuring the Client#
Now there is still one client function that hasn't been written. This is just to call the server settings for different languages, and the content is very simple.
Create a new config/dap/cpp.lua
and configure the parameters related to C++. It is important to note that codelldb can debug C, C++, Rust, and other languages, so it won't be split into more detailed files.
local M = {}
function M.setup()
-- local dap_install = require "dap-install"
-- dap_install.config("codelldb", {})
local dap = require("dap")
local install_root_dir = vim.fn.stdpath("data") .. "/mason"
local extension_path = install_root_dir .. "/packages/codelldb/extension/"
local codelldb_path = extension_path .. "adapter/codelldb"
dap.adapters.codelldb = {
type = "server",
port = "${port}",
executable = {
command = codelldb_path,
args = { "--port", "${port}" },
-- On windows you may have to uncomment this:
-- detached = false,
},
}
dap.configurations.cpp = {
{
name = "Launch file",
type = "codelldb",
request = "launch",
program = function()
return vim.fn.input("Path to executable: ", vim.fn.getcwd() .. "/", "file")
end,
cwd = "${workspaceFolder}",
stopOnEntry = true,
},
}
dap.configurations.c = dap.configurations.cpp
dap.configurations.rust = dap.configurations.cpp
end
return M
Mason finally makes an appearance here, but we only see the path to find the codelldb installation.
The configuration content is fixed; just set the path and parameters for the executable file and the startup parameters required for debugging this language. Here, a simple method is provided to input the executable file path to start debugging.
Configuring launch.json#
The above content is already sufficient to debug C++ programs, but dap also supports vscode's launch.json, allowing startup configurations to be filled in as fixed templates in the startup debugging list. In launch.json, we can also control the program's environment variables, startup parameters, etc., which is more convenient.
To support this in dap, simply add one line of code in the setup function.
require("dap.ext.vscode").load_launchjs(nil, { codelldb = { "c", "cpp", "rust" } })
This line means that when the type in launch.json is codelldb, it uses the debugging configurations for C, C++, and Rust. We have already configured the parameters for codelldb and cpp, and we also copied the cpp configuration for C and Rust.
However, one thing to note is that the environment variables in launch.json have now changed to the environment field, and the structure has also changed. Currently, dap only supports the env field, and I am considering contributing a PR to do an automatic conversion.
Here is an example of launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "(codelldb) Launch",
"type": "codelldb",
"request": "launch",
"program": "./build/bin/deepin-kwin_x11",
"args": [
"--replace"
],
"stopAtEntry": true,
"cwd": "${workspaceFolder}",
"env": {
"DISPLAY": ":0",
"PATH": "${workspaceFolder}/build/bin:$PATH",
"XDG_CURRENT_DESKTOP": "Deepin",
"QT_PLUGIN_PATH": "${workspaceFolder}/build",
"QT_LOGGING_RULES": "kwin_*.debug=true"
},
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}
It is important to note that here, codelldb is actually an identifier string. The default type provided by vscode is cppgdb, and we can also change it to the same field.