lxz

lxz

C++ | Typescript | 图形白痴

在 Neovim 裡用 dap 調試 c++

在之前我已經分享過了一份簡單的 nvim 配置,它已經實現了編程所需的智能提示,語法高亮,代碼跳轉等功能,今天我打算整一下 nvim 的調試框架 dap。

dap 是一個框架,客戶端負責在 nvim 上顯示各種調試信息,比如顯示斷點、調用棧、對象內存信息等,服務端則提供客戶端所需的功能,服務端通常是一個調試器,或者是調試器包裝。

本篇會用到 Mason 這個插件去安裝 dap 的服務端,本篇不會展開 Mason,將來有機會詳細說一下。

首先先看幾張正常工作的圖:

image

運行界面

image

查看變量信息

image

快捷鍵

image

函數調用棧

安裝 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

配置基本上大家都沒差多少,說不定都是從一個人的配置裡搬運的。

image

配置 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

image

斷點標記

image

單步停止

配置客戶端#

現在還差一個客戶端的函數沒有寫,在這裡只是為了調用針對不同語言設置的服務端,內容也非常的簡單。

新建一個 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,我們也可以改成相同的字段。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。