在之前我已經分享過了一份簡單的 nvim 配置,它已經實現了編程所需的智能提示,語法高亮,代碼跳轉等功能,今天我打算整一下 nvim 的調試框架 dap。
dap 是一個框架,客戶端負責在 nvim 上顯示各種調試信息,比如顯示斷點、調用棧、對象內存信息等,服務端則提供客戶端所需的功能,服務端通常是一個調試器,或者是調試器包裝。
本篇會用到 Mason 這個插件去安裝 dap 的服務端,本篇不會展開 Mason,將來有機會詳細說一下。
首先先看幾張正常工作的圖:
運行界面
查看變量信息
快捷鍵
函數調用棧
安裝 dap#
在 Mason 的安裝列表中添加上 codelldb,codelldb 是 vscode 用的調試服務端,負責給 vscode 提供調試信息,有了這個後端,我們就可以方便的實現和 vscode 相同的調試功能。
配置 dap#
在 plugins 目錄下新建 _dap.lua
文件。
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,
}
有些人會在 packer 裡用 use 安裝,把 return 改成 use 就可以了。
packer 的代碼已經寫好了,現在寫 config 函數,在我的例子中,我把文件放在了 lua/config/dap/
目錄下,因為要配置不同的語言,這樣會方便管理一些。
首先要先在 dap 目錄下新建一個 init.lua,這裡是模塊入口,初始化的工作從這裡開始。
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
在 _dap.lua
中調用了 require("config.dap").setup()
,這個 setup 函數就是 config/dap/init.lua
中的 M.setup()
函數。
目前只是寫了一個殼子,現在讓我們正式配置它吧。
快捷鍵#
在 nvim 中進行調試,界面顯然還是在終端裡的,所以我們要使用快捷鍵進行一些操作,比如標記斷點、單步進入、跳出等。
在 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
在這裡我將快捷鍵綁定在了 <leader> d
上面。
現在返回到 init.lua
中,在 setup
函數中調用 keymaps。
function M.setup()
require("config.dap.keymaps").setup() -- Keymaps
end
dapui#
dapui 是一個美化 dap 界面的插件,通常大家都會配置的吧!
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
配置基本上大家都沒差多少,說不定都是從一個人的配置裡搬運的。
配置 icon#
我還修改了幾個默認的 icon,在 configure 函數裡。
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
斷點標記
單步停止
配置客戶端#
現在還差一個客戶端的函數沒有寫,在這裡只是為了調用針對不同語言設置的服務端,內容也非常的簡單。
新建一個 config/dap/cpp.lua
,在裡面配置 c++ 相關的參數就行了,需要注意的是,codelldb 可以調試 c、c++、rust 等語言,就不會再拆分成更精細的文件了。
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 在這裡終於露面了,但是我們只是看到查找了 Mason 安裝 codelldb 的路徑而已。
配置的內容是固定的,設置一下執行文件的路徑和參數,設置一下調試這個語言所需的啟動參數,這裡默認給了一個輸入可執行文件路徑啟動調試的簡單方法。
配置 launch.json#
上面的內容就已經足夠調試 c++ 程序了,但是 dap 還支持 vscode 的 launch.json,將啟動配置作為固定模板填入啟動調試的列表,並且在 launch.json 中我們還可以控制程序的環境變量,啟動參數等,會比較方便一些。
dap 支持這個只需要在 setup 函數加上一行代碼就足夠了。
require("dap.ext.vscode").load_launchjs(nil, { codelldb = { "c", "cpp", "rust" } })
這句話的意思是 launch.json 中的類型是 codelldb 時,使用 c、cpp、rust 的調試配置,而上面我們配置了 codelldb 的參數 和 cpp 的參數,而且還將 cpp 的配置複製給了 c 和 rust。
但是有一個需要注意的地方,launch.json 現在環境變量換成了 environment 字段,並且結構也發生了變化,dap 目前只支持 env 字段,我在考慮貢獻一個 pr 做一個自動轉換。
這裡給一個 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
}
]
}
]
}
需要注意的是,這裡的 codelldb 其實是一個標識字符串,vscode 默認提供的 type 是 cppgdb,我們也可以改成相同的字段。