Neovim Debugger

5 minute read

Setting up Neovim debugger

In this article you will learn how to setup your Neovim to debug pretty much every programming language out there!


Overview

In this article we will learn how to configure and use nvim-dap which is a Debug Adapter Protocol client implementation for Neovim. I will choose C/C++/Rust languages as a demonstration but this will also apply to any other language.


What’s different between this article and others?

Most articles regarding this matter is that most of them descriping how to configure nvim-dap but not dealing with issues such as:

  • What if you want to give the executable name before start debugging?
  • What if you want to give arguments to the program?
  • What if you don’t want to use launch.json to configure every single debugging session?
  • What if you want to compile before run?

All these issues don’t get attention alot and I think it’s the reason why using a debugger in a text editor is so hard because you have to deal with these issues and more yourself.

My goal here is to simplify these issues and give you the way on how to extend them.


Getting Started

First of you will need to install some plugins using the plugin manager you prefer, if you are using vim-plug copy this to your init.vim, save the file, source it, and execute :PlugInstall

Plug 'nvim-treesitter/nvim-treesitter', {'do': ':TSUpdate'}
Plug 'mfussenegger/nvim-dap'
Plug 'rcarriga/nvim-dap-ui'
Plug 'theHamsta/nvim-dap-virtual-text'
Plug 'nvim-telescope/telescope-dap.nvim'

A brief description about these plugins:

  • nvim-treesitter: An incremental parsing system for programming tools.
  • nvim-dap: Debug Adapter Protocol client implementation for Neovim.
  • nvim-dap-ui: A UI for nvim-dap.
  • nvim-dap-virtual-text: Virtual text support to nvim-dap using treesitter to find variable definitions.
  • nvim-telescope/telescope-dap.nvim: Telescope support for nvim-dap.

Now as you successfully installed these plugins, now let’s configure our debugger to work with C/C++/Rust code.


Setting Up

To keep things organized and easy to maintain, I recommend using this structure, for more information about this structure see :help rtp, but basically any .vim or .lua file inside after/plugin gets sourced after init.vim.

.
├── after
│  └── plugin
│     └── debugger
│        ├── cpp.lua
│        └── init.lua
├── lua
│  └── keymap.lua
└── init.vim

And here is a note about thes files

  • lua/keymap.lua: A simple class to simplify remaps in lua.
  • after/plugin/debugger/init.lua: Here we keep the general configurations and remaps.
  • after/plugin/debugger/cpp.lua: here we keep a specific configurations for C/C++/Rust debugger. We keep this convention for any new language (e.g., go.lua). —

DAP Configuration

Let’s now start configuring our debugger!

Copy this to your lua/keymap.lua

local M = {}

local function bind(op, outer_opts)
    outer_opts = outer_opts or {noremap = true}
    return function(lhs, rhs, opts)
        opts = vim.tbl_extend("force",
            outer_opts,
            opts or {}
        )
        vim.keymap.set(op, lhs, rhs, opts)
    end
end

M.nmap = bind("n", {noremap = false})
M.nnoremap = bind("n")
M.vnoremap = bind("v")
M.xnoremap = bind("x")
M.inoremap = bind("i")

return M

Now, here is the content of after/plugin/debugger/init.lua

First we include all modules

local Remap = require("keymap")
local dap = require("dap")
local dapui = require("dapui")
local daptext = require("nvim-dap-virtual-text")
local telescope = require("telescope")

local nnoremap = Remap.nnoremap

Then we setup dap-ui, you can customize it to your taste

vim.fn.sign_define(
    "DapBreakpoint",
    { text = "●", texthl = "", linehl = "debugBreakpoint", numhl = "debugBreakpoint" }
)
vim.fn.sign_define(
    "DapBreakpointCondition",
    { text = "◆", texthl = "", linehl = "debugBreakpoint", numhl = "debugBreakpoint" }
)
vim.fn.sign_define("DapStopped", { text = "▶", texthl = "", linehl = "debugPC", numhl = "debugPC" })
dap.defaults.fallback.force_external_terminal = true
daptext.setup()
dapui.setup({
    layouts = {
        {
            elements = {
                "watches",
                { id = "scopes", size = 0.5 },
                { id = "repl", size = 0.15 },
            },
            size = 79,
            position = "left",
        },
        {
            elements = {
                "console",
            },
            size = 0.25,
            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 = "□",
        },
    },
})
telescope.load_extension("dap")

And finally your remaps, again customize it to your needs

-- Start
nnoremap("<F9>", function()
    dap.continue()
    dapui.open()
end)
-- Exit
nnoremap("<F7>", function()
    dap.terminate()
    dapui.close()
    vim.cmd("sleep 50ms")
    dap.repl.close()
end)
-- Restart
nnoremap("<F21>", function()
    dap.terminate()
    vim.cmd("sleep 50ms")
    dap.repl.close()
    dap.continue()
end) -- Shift F9
nnoremap("<leader>B", function()
    dap.set_breakpoint(vim.fn.input("Breakpoint condition: "))
end)
nnoremap("<F8>", function()
    dap.toggle_breakpoint()
end)
nnoremap("<F20>", function()
    dap.clear_breakpoints()
end) -- SHIFT+F8
nnoremap("<F10>", function()
    dap.step_over()
end)
nnoremap("<leader>rc", function()
    dap.run_to_cursor()
end)
nnoremap("<F11>", function()
    dap.step_into()
end)
nnoremap("<F12>", function()
    dap.step_out()
end)
nnoremap("<leader>dp", function()
    dap.pause()
end)
nnoremap("<leader>dtc", function()
    telescope.extensions.dap.commands({})
end)

C/C++/Rust Debugger

Now, let’s add support to C/C++/Rust languages.

In this step you have basically two options

  • Using codelldb: (recommended) Go to releases and download the latest version (codelldb-x86_64-linux.vsix). Unpack it by changing its extension to .zip and make a simlink for codelldb-x86_64-linux/extension/adapter/codelldb to /usr/bin/codelldb to be in your PATH.

  • Using vscode-cpptools: do the same as previous but create a simlink for cpptools-linux/extension/debugAdapters/bin/OpenDebugAD7 to /usr/bin/OpenDebugAD.

Now, here is the content of after/plugin/debugger/cpp.lua

Firstly, configure the debug adapter

local dap = require("dap")
-- vscode-cpptools
dap.adapters.cppdbg = {
    id = "cppdbg",
    type = "executable",
    command = "/usr/bin/OpenDebugAD",
}
-- codelldb
dap.adapters.codelldb = {
    type = "server",
    port = "${port}",
    executable = {
        command = "/usr/bin/codelldb",
        args = { "--port", "${port}" },
    },
}

Secondly, configure for each language. This is for C/C++

dap.configurations.cpp = {
    {
        -- Change it to "cppdbg" if you have vscode-cpptools
        type = "codelldb",
        request = "launch",
        program = function ()
            -- Compile and return exec name
            local filetype = vim.bo.filetype
            local filename = vim.fn.expand("%")
            local basename = vim.fn.expand('%:t:r')
            local makefile = os.execute("(ls | grep -i makefile)")
            if makefile == "makefile" or makefile == "Makefile" then
                os.execute("make debug")
            else
                if filetype == "c" then
                    os.execute(string.format("gcc -g -o %s %s", basename, filename))
                else
                    os.execute(string.format("g++ -g -o %s %s", basename, filename))
                end
            end
            return basename
        end,
        args = function ()
            local argv = {}
            arg = vim.fn.input(string.format("argv: "))
            for a in string.gmatch(arg, "%S+") do
                table.insert(argv, a)
            end
            vim.cmd('echo ""')
            return argv
        end,
        cwd = "${workspaceFolder}",
        -- Uncomment if you want to stop at main
        -- stopAtEntry = true,
        MIMode = "gdb",
        miDebuggerPath = "/usr/bin/gdb",
        setupCommands = {
            {
                text = "-enable-pretty-printing",
                description = "enable pretty printing",
                ignoreFailures = false,
            },
        },
    },
}
-- You can even copy configurations
dap.configurations.c = dap.configurations.cpp

And this is for Rust

dap.configurations.rust = {
    {
        type = "codelldb",
        request = "launch",
        -- This is where cargo outputs the executable
        program = function ()
            os.execute("cargo build &> /dev/null")
            return "target/debug/${workspaceFolderBasename}"
        end,
        args = function ()
            local argv = {}
            arg = vim.fn.input(string.format("argv: "))
            for a in string.gmatch(arg, "%S+") do
                table.insert(argv, a)
            end
            return argv
        end,
        cwd = "${workspaceFolder}",
        -- Uncomment if you want to stop at main
        -- stopOnEntry = true,
        MIMode = "gdb",
        miDebuggerPath = "/usr/bin/gdb",
        setupCommands = {
            {
                text = "-enable-pretty-printing",
                description = "enable pretty printing",
                ignoreFailures = false,
            },
        },
    },
}

At this point you have a very powerful debugger in your neovim, you can add support to other languages by creating a file under after/plugin/debugger/language.lua and refer to nvim-dap wiki for all the information you need to do so.