很抱歉这么久才来更新这一系列,主要是来新公司还在试用期,我希望在试用期干出点事来,所以摸鱼的时间就少了。加上前面自己阳了休息了一段时间。在想起来更新就过去一个多月了。废话不多说了,让我们开始进入正题。

在前一章,我们谈论了如何在 ​​neovim​​​ 中使用​​cpptools​​​ 这个​​DAP​​​ 的适配器对代码进行调试,目前针对编译型和解释型语言来说我们都有了对应的方法来配置调试器对其进行调试。本节将要介绍关于​​dap​​​的其他一些功能,主要包括 ​​repl​​​窗口和 ​​gdb​​的集成

repl 窗口

什么是 ​​repl​​​ 呢?它的全称是 ​​Read Eval Print Loop​​​ 中文一般翻译为交互式解析器,可能看到这你还是一脸懵逼,你可以想想 ​​python​​​ 或者​​nodejs​​​,在控制台输入​​python​​​ 就可以进入到它的交互式解析器中,随着我们输入​​python​​ 的语句,它会实时的给出运行的结果。交互式解析器就是这么一个东西,输入命令,它给你一个实时的结果。在调试中使用交互式解析器还是很有用的,比如我想显示当前某个变量的值,当前执行到哪个语句了等等。

​nvim-dap​​​已经提供了一个内置的 ​​repl​​​ 窗口,我们每次启动调试的时候都会看到它每次都会创建一个新的名为 ​​dap-repl​​​的​​buffer​​。之前我们没怎么关注它主要还是因为那个时候的重点在于如何进行调试,现在我们将要来优化这部分的显示和功能。

首先我们发现每次调试结束的时候这个​​buffer​​​ 都会被遗留,需要我们手动的进行关闭,除了针对​​buffer​​​通用的 ​​:q​​​ 或者 ​​:bd​​​命令进行关闭,还可以使用 ​​:lua require('dap').repl.close()​​​。当然也可以配套使用 ​​:lua require('dap').repl.open()​​来打开一个 repl的窗口,既然每次都会自动新建,那么这里我们就不需要进行新建,主要用于想办法关闭就可以了。

还记得之前介绍 ​​nvim-dapui​​​ 插件的时候介绍的那两个监听函数吗,同样的我们要在监听调试结束的函数中添加代码来关闭​​repl​​ 窗口,函数的整个代码如下

dap.listeners.before.event_terminated["dapui_config"] = function()
dapui.close({})
dap.repl.close()
end

dap.listeners.before.event_exited["dapui_config"] = function()
dapui.close({})
dap.repl.close()
end

再启动调试的时候发现它已经可以在调试结束的时候自动关闭了。

从零开始配置vim(30)——DAP的其他配置_快捷键

还有另外一个问题就是我不太喜欢现在这样在最下角显示 ​​repl​​​。我希望它能够在最下方以整行的形式显示。或者可以方便的不显示,只有在需要的时候显示。要达成这个目的我们需要修改 ​​dapui​​​ 的配置。
​​​dapui​​​ 的配置主要以 ​​element​​​为基础,每个 ​​element​​ 代表一个提供对应功能的窗口。我们先来根据它默认的配置来讲解每部分的含义

require("dapui").setup({
icons = { expanded = "", collapsed = "", current_frame = "" },
mappings = {
-- Use a table to apply multiple mappings
expand = { "<CR>", "<2-LeftMouse>" },
open = "o",
remove = "d",
edit = "e",
repl = "r",
toggle = "t",
},
-- Use this to override mappings for specific elements
element_mappings = {
-- Example:
-- stacks = {
-- open = "<CR>",
-- expand = "o",
-- }
},
-- Expand lines larger than the window
-- Requires >= 0.7
expand_lines = vim.fn.has("nvim-0.7") == 1,
-- Layouts define sections of the screen to place windows.
-- The position can be "left", "right", "top" or "bottom".
-- The size specifies the height/width depending on position. It can be an Int
-- or a Float. Integer specifies height/width directly (i.e. 20 lines/columns) while
-- Float value specifies percentage (i.e. 0.3 - 30% of available lines/columns)
-- Elements are the elements shown in the layout (in order).
-- Layouts are opened in order so that earlier layouts take priority in window sizing.
layouts = {
{
elements = {
-- Elements can be strings or table with id and size keys.
{ id = "scopes", size = 0.25 },
"breakpoints",
"stacks",
"watches",
},
size = 40, -- 40 columns
position = "left",
},
{
elements = {
"repl",
"console",
},
size = 0.25, -- 25% of total lines
position = "bottom",
},
},
controls = {
-- Requires Neovim nightly (or 0.8 when released)
enabled = true,
-- Display controls in this element
element = "repl",
icons = {
pause = "",
play = "",
step_into = "",
step_over = "",
step_out = "",
step_back = "",
run_last = "",
terminate = "",
},
},
floating = {
max_height = nil, -- These can be integers or a float between 0 and 1.
max_width = nil, -- Floats will be treated as percentage of your screen.
border = "single", -- Border style. Can be "single", "double" or "rounded"
mappings = {
close = { "q", "<Esc>" },
},
},
windows = { indent = 1 },
render = {
max_type_length = nil, -- Can be integer or nil.
max_value_lines = 100, -- Can be integer or nil.
}
})

首先最上面的 ​​icons​​​ 表示,各个部分显示的图标,这里分别定义了展开的,合并的以及当前位置的图标信息,我们可以观察一下变量栏或者调用栈显示信息的左侧就可以看到这里定义的图标。
​​​mappings​​ 代表的是部分窗口动作定义的快捷键。例如上面定义的 expand = { "<CR>", "<2-LeftMouse>" }表示可以在待展开项上按下回车或者鼠标左键双击来展开。

​element_mappings​​​表示的是我们为某些窗口特意定制的一些快捷键。例如上面注释的是针对调用栈定义的快捷键。
​​​layouts​​​代表布局,每个布局都有一个子​​table​​​,而每个子​​table​​​主要由 ​​elements​​​、​​size​​​、​​position​​​组成,它们分别代表采取该种布局的元素(也可以说是窗口),窗口大小以及窗口的位置。窗口一般通过​​id​​来描述,每种窗口都有固定的ID,根据官方文档的描述,它支持这么几种窗口:

  • ​scopes​​​显示全局或者当前局部变量,它支持的操作主要是 ​​edit​​​编辑变量的值、​​expand​​​展开结构化的变量、​​repl​​​将变量拷贝到​​repl​​窗口
  • ​stacks​​​显示当前正在运行的线程以及它们对应的调用栈,它主要支持的操作是 ​​open​​​ :运行代码到当前被选中的位置, ​​toggle​​:打开或者关闭该窗口
  • ​watches​​​显示我们需要追踪的变量,它支持的主要操作是 ​​edit​​​: 输入想要追踪的变量或者给对应的变量赋值。​​expand​​​: 展开结构化的变量,​​remove​​​:删除当前监视的变量,​​repl​​​:将变量拷贝到​​repl​​窗口
  • ​breakpoints​​​显示当前激活的断点。它支持的主要操作有 ​​open​​​:执行代码到当前选中的断点处, ​​toggle​​ :激活或者使当前断点无效
  • ​repl​​显示repl窗口
  • ​console​​​显示控制台窗口
    这些窗口的这些操作的快捷键我们已经通过上方的 ​​​mappings​​​做了定义了,只要保持光标在对应窗口然后按下快捷键就可以执行对应的窗口命令了。如果想要单独对窗口进行快捷键定义可以在​​element_mappings ​​ 中被注释的代码

​controls​​​ 部分配置的是在repl窗口上方显示的那一堆调试按钮。由于我们定义了一些快捷键,这些按钮没太大的作用。这里我不需要它显示调试用的按键,所以我就在 ​​controls​​​ 项中设置 ​​enabled = false​​ 禁用它。

​floating​​​、​​window​​​、​​render​​则定义的是悬浮窗口样式和普通窗口的一些样式。这里就不深究了。

下面是改造之后的配置

dapui.setup({
icons = { expanded = "", collapsed = "", current_frame = "" },
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.35
},
{id = "stacks", size = 0.35},
{id = "watches", size = 0.15},
{id = "breakpoints", size = 0.15},
},
size = 40,
position = "left",
},
{
elements = {
"repl"
},
size = 5,
position = "bottom",
}
},

controls = {enabled = false},
floating = {
max_height = nil, -- These can be integers or a float between 0 and 1.
max_width = nil, -- Floats will be treated as percentage of your screen.
border = "single", -- Border style. Can be "single", "double" or "rounded"
mappings = {
close = { "q", "<Esc>" },
},
},
windows = { indent = 1 },
})

​repl​​ 窗口的主要命令如下:

  • ​.exit​​: 退出/关闭一个 repl 窗口
  • ​.c/.continue​​: 继续执行代码
  • ​.n/.next​​: 执行下一行代码
  • ​.into​​: 跳转到函数中继续执行
  • ​.out​​: 跳出函数
  • ​.scopes​​: 打印当前栈的一些变量信息
  • ​.threads​​: 打印线程信息
  • ​.frames​​: 打印当前线程的调用栈
  • ​.capabilities​​: 打印当前适配器实现的一些功能
  • ​.p​​ 暂停当前运行的程序

更多的命令可以通过在 ​​repl​​​窗口中输入 ​​.help​​​查看
看了这么多无聊的文字描述,不知道小伙伴们有没有觉得头晕眼花呢?我们来通过实际的例子来看看如何应用这些内容来进行调试。

示例1:调试单线程死循环

假设有一段程序在不知不觉中被写成死循环了,程序无法正常执行下面的操作,我们以下面的程序为例

#include <stdio.h>
#include <unistd.h>

void loop_forever(){
for(unsigned int i = 10; i >= 0; --i){
// do something
sleep(0.1);
}
}

int main (int argc, char *argv[])
{
// do something
loop_forever();
printf("do other things\n");
return 0;
}

本来我们想的是它执行完函数 ​​loop_forever​​之后会执行接下来的操作,但是我们死活看不到它执行后面的操作,这个时候我们意识到它可能在某个地方陷入死循环,无法出来了,假设前后都有大断的代码,无法快速定位到死循环的位置,该如何处理这种情况呢?

我们先通过​​<F5>​​​来执行操作,然后在 ​​repl​​​ 中输入 ​​i​​​ 进入插入模式,然后执行​​.p​​ 中断当前程序执行。

从零开始配置vim(30)——DAP的其他配置_快捷键_02

此时程序已经断了下来,接着我们输入​​.frames​​ 查看当前调用栈信息。

从零开始配置vim(30)——DAP的其他配置_调用栈_03


我们发现此时程序停留在​​loop_forever​​​ 函数的 ​​sleep​​​ 中,我们在栈中找到 ​​sleep​​​ 的位置并按下回车,这个时候我们发现程序执行到了 ​​sleep()​​​ 函数处了。这个时候我们在这里按下​​<F9>​​​下一个断点,接着使用 ​​<F5>​​​ 继续运行到断点位置停止,这个时候我们通过实时显示的 ​​i​​值已经发现问题所在了。

从零开始配置vim(30)——DAP的其他配置_调用栈_04


原来​​i​​​ 递减到​​0​​​之后,继续递减,因为它是无符号数所以永远无法达成小于​​0​​ 的条件。

示例2:调试多线程死锁

我们以下面一个多线程程序为例

#include <cstdio>
#include <thread>
#include <vector>

using namespace std;
void func(int thread_id){
int var = thread_id;
while(true);
}
int main (int argc, char *argv[])
{
vector<thread> vec;
for(int i = 0; i < 5; i++){
vec.push_back(thread(func, i));
}

// join
for(int i = 0; i < 5; i++){
vec[i].join();
}

return 0;
}

因为使用了​​c++​​​ 相关的内容和 ​​thread​​​ 库,因此编译的时候需要使用 ​​g++​​​ 并且指定链接 ​​pthread​​​ 库,我们采用 ​​g++ main.cpp -g -o main -lpthread​​ 来编译它

我们还是按照之前的步骤,先按下 ​​.p​​来暂停程序,这个时候我们发现它会提示我们需要暂停哪个线程,遗憾的是根据线程的id还没法判断具体哪个是子线程哪个是主线程。这里我们随便选一个暂停。

从零开始配置vim(30)——DAP的其他配置_快捷键_05


从零开始配置vim(30)——DAP的其他配置_调用栈_06


然后执行 ​​.threads​​​查看当前线程信息,在某个线程下使用回车键可以看到调用的函数栈。我们发现子线程卡在​​while​​​ 这句话,我们还是一样在卡主的位置按下回车跳转到对应代码位置,在此处下一个断点。然后我们在对应线程位置按下 ​​o​​ 命令来继续执行之前暂停的线程。这样就完美的找到了线程卡死的位置了。

从零开始配置vim(30)——DAP的其他配置_快捷键_07


从零开始配置vim(30)——DAP的其他配置_vim_08


后面可以使用 ​​.c​​ 来继续执行所有被中断的线程

nvim-gdb 插件

该插件提供了一种方式,可以直接在neovim中进入gdb的session。例如我们可以通过命令 ​​:GdbStart gdb -q a.out​​​来启动一个​​gdb​​​会话,并且关联了一个 ​​a.out​​​ 的程序。后续可以直接使用​​gdb​​相关的命令来启动调试这个程序。我们可以使用如下的代码来安装它

use {"sakhnik/nvim-gdb", run="./install.sh"}

我们先来试试效果,直接使用快捷键 ​​<leader>dd​​​ 来加载一个程序进行调试。进入到​​gdb​​​会话之后可以使用​​gdb​​​ 的命令。例如我们使用 ​​b main​​​来在 ​​main​​​函数的位置打一个断点,然后通过​​r​​​来启动程序运行到断点处。接着可以使用 ​​n​​​来执行下一步或者使用 ​​c​​​来直接运行到下一个断点。最后可以使用 ​​q​​退出

从零开始配置vim(30)——DAP的其他配置_vim_09

基础配置

我们发现使用 ​​nvim-gdb​​​ 插件的时候会在对应代码位置显示断点或者当前执行行。这里我们对它做一些配置,先统一使用​​nvim-gdb​​​和 ​​nvim-dap​​​这两种情况下的显示信息。
先创建一个新的配置文件为 ​​​nvimgdb.lua​​​作为它的配置文件。
因为它暂时还不支持​​​lua​​​的配置所以这里我们使用​​vim​​原生的写法。

vim.cmd([[
let g:nvimgdb_config_override = {
\ 'sign_breakpoint': [''],
\ 'sign_current_line': '',
\ }
]])

在这里定义了断点和执行到当前行的配置,这里只能统一图标,还无法做到完全像使用 dap那样显示。
另外我们可以在 ​​​nvimgdb_config_override​​ 这个变量中定义如下快捷键以保证在调试时拥有同样的体验

\ 'key_next': '<F10>',
\ 'key_step': '<F11>',
\ 'key_continue': '<F5>',

从零开始配置vim(30)——DAP的其他配置_调用栈_10


最后我们发现当我们输入文件名进行调试的时候,它会一直闪屏。这是因为它会不断根据我们输入的内容在文件系统中匹配合适的可执行文件,为了解决闪屏问题我们屏蔽它的这个特性,我们可以使用如下的配置来解决这个问题

let g:nvimgdb_use_find_executables = 0
let g:nvimgdb_use_cmake_to_find_executables = 0

从零开始配置vim(30)——DAP的其他配置_快捷键_11

显示窗口的配置

定义了显示形式,我们来定义显示的窗口,这里我让它显示常用的像调用栈,变量,以及watch窗口。我们先使用如下代码来将窗口分割为左右两个部分

let w:nvimgdb_termwin_command = "rightbelow vnew"
let w:nvimgdb_codewin_command = "vnew"

根据官方文档的描述,​​nvimgdb_termwin_command​​​ 是终端窗口,用来显示​​repl​​​ 相关信息并且与用户交互,后面我们可以对它进行切割用于显示其他信息。​​nvimgdb_codewin_command​​ 是源代码窗口。这两句代码可以形成一个左右分屏的界面,左侧显示代码,右侧显示repl窗口。

从零开始配置vim(30)——DAP的其他配置_调用栈_12


在​​gdb​​​成功加载之后,我们可以使用命令 ​​:GdbCreateWatch info locals​​​来创建一个显示当前变量的窗口。默认是新创建一个窗口来显示,但是我们可以在命令前加上 ​​belowright​​​来指定在右侧继续分屏,它会在右下角新建窗口来显示变量。需要查看其它窗口可以对应传入不同的参数,例如传入 ​​breakpoints​​来显示所有断点信息。传入的参数就是gdb中接收的对应参数。

从零开始配置vim(30)——DAP的其他配置_调用栈_13

有了这些基础我们就可以对其进行配置了,我们要实现的目标就是当​​gdb​​​成功加载的时候自动加载这些窗口。在​​vim​​​中要实现自动化我们目前知道有两种方式,第一种使用自动命令,第二种使用插件配置中提供的回调函数。遗憾的是在这个插件中我没有找到回调函数,因此我们只能采用自动命令这种方法。根据官方的文档,我们主要使用这么两个事件——​​NvimGdbStart​​​和 ​​NvimGdbCleanup​​​。它们一个是成功加载​​gdb​​​的时候触发,一个是关闭​​gdb​​会话的时候触发。

vim.cmd([[augroup GdbSession
autocmd!
autocmd User NvimGdbStart :lua StartGdbSession()
autocmd User NvimGdbCleanup :lua EndGdbSession()
augroup END]])

在 ​​StartGdbSession​​ 函数中写入如下代码来完成窗口的配置

StartGdbSession = function()
vim.api.nvim_command(":belowright GdbCreateWatch backtrace")
vim.api.nvim_command(":wincmd h")
vim.api.nvim_command(":belowright GdbCreateWatch info locals")
vim.api.nvim_command(":set wrap")
vim.api.nvim_command(":wincmd k")
end

不知道各位小伙伴能不能理解这段代码是如何在分屏的。
首先启动​​​gdb​​​的时候会将整个屏幕纵向分为两个部分,左侧为 ​​code​​​右侧为 ​​repl​​​窗口
接着我们执行 ​​​:belowright GdbCreateWatch backtrace​​​ 它会在右下方创建一个窗口用来展示调用栈
然后执行 ​​​:wincmd h​​​来将光标移动到代码窗口上
继续执行 ​​​:belowright GdbCreateWatch info locals​​它会在代码窗口的下方新增一个窗口用于显示变量的信息

这样就将窗口分为4个部分了,左上部分显示代码,左下部分显示变量信息。右上部分是repl窗口,右下部分显示变量信息。

最后我们通过 ​​:set wrap​​​设置窗口中自动换行,不然有些内容显示在一行不容易查看。通过 ​​:wincmd k​​移动光标到 repl窗口。方便后续调试

启动之后他的效果如下

从零开始配置vim(30)——DAP的其他配置_vim_14


最后我们在结束​​gdb​​的时候做一些收尾工作,关闭我们创建的窗口

EndGdbSession = function()
vim.api.nvim_command(":bdelete! backtrace info locals")
end

这里我是根据buffer的名称来进行删除。最后的效果如下

从零开始配置vim(30)——DAP的其他配置_快捷键_15

到此我们已经介绍了关于​​dap​​ 的所有配置,至于其他语言相信各位小伙伴根据官方给出的示例可以独立完成配置,这里就不一一介绍了。