lxz

lxz

C++ | Typescript | 图形白痴

Debugging C++ with dap in Neovim

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:

image

Running interface

image

Viewing variable information

image

Shortcut keys

image

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 change return to use.

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.

image

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

image

Breakpoint markers

image

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.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.