NeovimでDAPを使ったデバッグ環境の構築と使い方
Neovimでデバッガーを動かすための諸々の設定方法と使い方を解説します。

デバッガーを動かすなら欠かせないDAPについて先に触れましょう。
DAPとは?
DAPはDebug Adapter Protocolの略で、Microsoftが提唱する「デバッグツールとエディタ間でのやりとり」に関する取り決めのことです。早い話、LSPのデバッグ版です。
デバッガーとエディタ間を橋渡しするアダプタ(LSPでいうとLanguage Server)をインストール・設定して使います。
NeovimでDAPを導入するには?
NeovimのDAPで登場するプラグインについて、それぞれの役割をまとめました。
nvim-dap
まず必須なのはnvim-dapです。
nvim-dapはどの言語でどのアダプタを使うのかを設定します。LSPでいうとnvim-lspconfigのような役割です。実際にデバッガーを起動したり操作するAPIなども提供します。
nvim-dap-ui
入れるのを強くおすすめするのがnvim-dap-uiです。
ボタンでポチポチしてnvim-dapのAPIを呼び出せます。変数やブレークポイントの表示もしてくれます。
画像のコード以外のウィンドウはnvim-dap-uiによる表示です。

nvim-dap-virtual-text
入れると便利なのがnvim-dap-virtual-textです。
デバッグで一時停止したときに「どの変数にどんな値が入っているか」などをvirtual textで表示してくれます。

導入方法
各々が使っているプラグインマネージャーでインストールします。
先にリポジトリ名のコピペ欄を載せておきます。
mfussenegger/nvim-dap
rcarriga/nvim-dap-ui
theHamsta/nvim-dap-virtual-text
nvim-dap-uiは依存プラグインもあります。
nvim-neotest/nvim-nio
lazy.nvimでのインストール例
lazy.nvimだと次のように書いてまとめてインストールできます。
return { "rcarriga/nvim-dap-ui", dependencies = { "mfussenegger/nvim-dap", "nvim-neotest/nvim-nio", { "theHamsta/nvim-dap-virtual-text", opts = {} }, }, opts = {},}
遅延読み込みの設定は後で紹介します。
アダプタの登録
まずはnvim-dapを使ってどの言語でどのアダプタを使うのかを設定します。
「デバッグしたい言語にはどんなアダプタがあるのか」「どうやって設定すればいいか」はnvim-dapのwikiのDebug Adapter installationにたくさん例が載っています。
ここではC++を例に挙げます。
local dap = require("dap")-- dapへアダプタを登録dap.adapters.codelldb = { type = "executable", command = "codelldb",}-- ファイルタイプ・アダプタ・設定の紐づけ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 = false, },}
分解して見ていきましょう。
アダプタを登録する
まずはアダプタを登録します。今回はcodelldbというアダプタをコマンドとして実行するため、次のように設定しました。
dap.adapters.codelldb = { type = "executable", command = "codelldb",}
もし引数が必要であればargs
に書きます。
dap.adapters.codelldb = { type = "executable", command = "codelldb", args = { "--foo", "1" },}
アダプタをインストール
アダプタのインストールも忘れずに。今回はmason.nvimを使ってインストールしました。
:MasonInstall codelldb
masonでインストールできるアダプタはmason-registry.dev category:dapから確認できます。
言語とアダプタを紐づける
続いて、言語ごとに「どのアダプタをどうやって使うか」設定します。
local dap = require('dap')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 = false, },}
これもwikiに書いてあります。name
・type
・request
が全アダプタで必須です。他のプロパティはアダプタによって異なります。
たとえばcodelldbでは、program
に実行ファイルを指定します。その他のcodelldb起動時に指定できるオプションはLaunching a New Processに書いてあります。
ビルドの設定
ビルドが必要な言語では、ビルド時にデバッグを有効にするためのオプションを設定します。
たとえばg++
でビルドするなら-g
をつけます。加えて、-O0
をつけて最適化を無効化にします(最適化するとコードの入れ替えなどが起き、デバッグしにくくなるそう)。
g++ -g -O0 main.cpp -o main.out
「デバッグの前にいちいち手動でビルドするなんて面倒じゃ!」という人向けのカスタマイズは後で紹介します。
参考:gcc+gdbによるプログラムのデバッグ 第1回 ステップ実行、変数の操作、ブレークポイント
使い方
まずは実際にデバッグする流れをざっくりと書いておきます。
- (ビルドが必要な言語なら)デバッグを有効にしてビルド
- ブレークポイントを設定
- デバッグ開始
- 随時操作&値の確認
- 入力が必要ならコンソールへ入力
- ステップオーバーなど、デバッグを進行させる
- 値を確認
練習用のコード
練習用にC++のコードも用意しました。
#include <iostream>using namespace std;
int add(int a, int b) { return a + b;}
int main() { int A, B; cin >> A >> B;
int result = add(A, B); cout << result << endl;
result = add(A, 7); cout << result << endl;}
デバッグの基本操作
nvim-dap-uiのボタンが表示されない場合、後半のトラブルシューティングを読んでください。
ブレークポイントの設定・解除
toggle_breakpoint
でブレークポイントの設定・解除ができます。
require("dap").toggle_breakpoint()
:DapToggleBreakpoint
ログメッセージの追加
ある行まで来たらメッセージをログとして出力したい場合は次のAPIを呼び出します。
require("dap").set_breakpoint(nil, nil, vim.fn.input("Log point message: "))
設定時にログメッセージが入力できます。
signcolumnに表示される
デフォルトだと設定した行のsigncolumnにB
(ブレークポイント)・L
(ログ)と表示されます。

このsignをアイコンに変更するカスタマイズは後で紹介します。
デバッガーの起動
デバッガーを起動する前に、nvim-dap-uiによるUIを表示しておくと便利です。
require("dapui").toggle()
UIを自動で表示させる方法は後で紹介します。
デバッグの開始はAPIかコマンドを呼び出すか、nvim-dap-uiのplayボタンを押します。
require("dap").continue()
:DapContinue

実行するとどうなる?知らんのか
開始すると、入力待機やブレークポイントまで実行されます。
動画は「入力を2つ要求するコード」でのデバッグ例です。画面右下にあるコンソールから入力します。
12行目で止まり、playボタンを押したら最後まで実行されています。
ブレークポイントで止まるとどうなる?知らんのか
画像は12行目のブレークポイントで止まっている例です。

つまり、11行目までが実行済み、12行目は実行されていない状態です(なのでvirtual-textの表示も32767という代入前の値があったりする)。
変数などを一通り確認し終わったら、continue
のAPIやコマンドまたはplayボタンを押して次のブレークポイントへ進みましょう。
入力が必要なかったりブレークポイントが設定されていなければ最後まで実行されます。
ログメッセージはREPLに表示されます。

ステップ〇〇のまとめ
ステップ〇〇系で処理を詳しく辿れます。ボタン・APIは次のとおりです。

require("dap").step_over() -- 次のステップ(行)まで進めるrequire("dap").step_into() -- 関数などの中へrequire("dap").step_out() -- 関数などから外へrequire("dap").step_back() -- 1ステップ戻る
:DapStepOver:DapStepInto:DapStepOut# backはコマンドがないっぽい
ブレークポイントで止まった後、1行ずつ処理を追いたい場合にstep_over
を使います。
「現在止まっている行に関数があって、その関数の中の処理まで追いたい」
そんなケースではstep_into
を使い、戻るときにstep_out
を使います。
step_back
は1ステップ戻るのですが、DAPによって対応がまちまちです。
ステップ〇〇の実際の動き
実際の動きを見てみましょう。
動画の例では、step_into
→stop_out
→step_over
を順に実行しています。
virtual-textが便利
nvim-dap-virtual-textを入れているとループ変数などが表示できて便利です。

画像の例では配列のサイズも表示されています(vectorの行)。
デバッグの終了
「もういいや」となったらterminate
かdisconnect
で終了させます。

require("dap").terminate()require("dap").disconnect()
:DapTerminate:DapDisconnect
terminate
terminate
はクリーンアップしてから終了します。たとえば「何かの登録処理中だったらそれが終わるまで待ってから終了するからね」という感じ。
disconnect
一方、disconnect
はやや複雑です。アダプタがはプログラムと切り離されます。このとき、プログラムが続行するかどうかはアダプタの種類によって変わります。
アダプタの起動方法がrequest: launch
ならプログラム終了、request: attach
なら終了させません。
local dap = require('dap')dap.configurations.cpp = { { name = "Launch file", type = "codelldb", request = "launch", -- ... },}
terminateとdisconnectどっちで停止させればいい?
デバッグ・プログラムを両方止めたいならとりあえずはterminate
の方で停止すれば良さそうです。
詳しく知りたい人はDAPの仕様書や:help dap.terminate()
を読みましょう。
参考:Requests_Disconnect | Microsoft DAP specification
カスタマイズ
ここからはカスタマイズの例を紹介します。
signcolumnの変更
DAPのsigncolumnの文字をアイコンに変更するには、vim.fn.sign_define
で設定します。
vim.fn.sign_define("DapBreakpoint", { text = "", texthl = "", linehl = "", numhl = "" }) vim.fn.sign_define("DapBreakpointRejected", { text = "", texthl = "", linehl = "", numhl = "", }) vim.fn.sign_define("DapLogPoint", { text = "", texthl = "", linehl = "", numhl = "" }) vim.fn.sign_define("DapStopped", { text = "", texthl = "", linehl = "", numhl = "" })
Nerd Fontsのアイコンを探したい人は公式サイトの検索を使うとよいでしょう。
ボタンアイコンの変更
nvim-dap-uiのボタンのアイコンもカスタマイズできます。
筆者の環境ではdisconnect
が見づらかったので変更しました。
require("dapui").setup({ controls = { icons = { disconnect = "⏻", }, }})
ステータスラインとの連携
DAPの情報をステータスラインに表示できます。どこで止まっているのかが分かって便利です。

ここではlualine.nvimの例を挙げます。
lualine_x = { { function() return require("dap").status() end, icon = { "", color = { fg = "#afdf00" } }, cond = function() if not package.loaded.dap then return false end local session = require("dap").session() return session ~= nil end, },},
色の指定は各自の環境に合わせて変更しましょう。
参考:is it possible to have a lualine indicator…… : r/neovim
overseerで自動ビルド
ビルドが必要な場合、毎回手動でやるのは面倒です。そこで、タスクランナーのプラグインoverseer.nvimを使って自動でビルドさせましょう。
overseer.nvimを知らない人・使ってみたい人は別の記事Neovimタスクランナーoverseer.nvimの使い方をどうぞ。
configurations
の各言語の設定でpreLaunchTask
にoverseer.nvimのテンプレート名を書くだけです。
dap.configurations.cpp = { { name = "Launch file", type = "codelldb", -- 省略 preLaunchTask = "g++ debug build", },}
こうすると、デバッグを開始する前にg++ debug build
のタスクを実行してくれます。
テンプレートg++ debug build
は次のような感じです。
---@type overseer.TemplateDefinitionreturn { name = "g++ debug build", builder = function() local file = vim.fn.expand("%:p") local outfile = vim.fn.expand("%:p:r") .. ".out" ---@type overseer.TaskDefinition return { cmd = { "g++" }, -- デバッグフラグ-g、最適化無効-O0 args = { "-g", "-O0", file, "-o", outfile }, components = { { "on_output_quickfix", open_on_exit = "failure" }, -- 通知しない { "on_complete_notify", statuses = {} }, "default", }, } end, -- DapのpreLaunchTaskから自動呼ぶため、優先度は低くする priority = 1000, condition = { filetype = { "cpp" }, },}
前述のとおり「デバッグ向け」のフラグをつけましょう。
デバッグ時にビルドの完了通知は煩わしいため、オフにしています。
優先度は下げています。このテンプレートは:OverseerRun
のリストから呼ぶことはほとんど無いからです。
templates
への登録も忘れずに。
require("overseer").setup({ templates = { "builtin", "cpp.gpp-debug-build", -- ... },})
ディレクトリによって実行ファイルを変更
特定のディレクトリで実行ファイルの指定を固定したい場合、次のように分岐してあげます。
dap.configurations.cpp = { { -- 省略 program = function() if vim.fn.getcwd():find("^" .. vim.fn.expand("~/ghq/github.com/eetann/example")) then return vim.fn.getcwd() .. "/main.out" end return vim.fn.input("Path to executable: ", vim.fn.getcwd() .. "/a.out", "file") end, -- 省略 },}
単純ですが楽になります。
逆アセンブルを非表示にする
codelldbでは、デバッグ情報が使えない部分は逆アセンブルの表示が出てきます(最後までステップオーバーしてみると出てきたりする)。
筆者は必要ないため、次のように引数を渡して非表示にしています。
dap.adapters.codelldb = { type = "executable", command = "codelldb", args = { "--settings", vim.json.encode({ showDisassembly = "never", }), },}
codelldbの設定はMANUAL.mdに書いてあります。
lazy.nvimので遅延読み込み・キーマップの紹介
keys
を使ってキーマップを定義しています。実際に使っているのは上4つだけです。ほかはdap-uiのボタンでポチポチしています。
keys = { { "<space>du", function() require("dapui").toggle() end, desc = "toggle dap-ui", }, { "<space>dd", function() require("dap").continue() end, desc = "Debug: start/continue", }, { "<space>dm", function() require("dap").toggle_breakpoint() end, desc = "Debug: toggle break(mark)", }, { "<space>dM", function() require("dap").set_breakpoint(nil, nil, vim.fn.input("Log point message: ")) end, desc = "Debug: set break(mark) with message", }, { "<space>de", function() require("dapui").eval() end, desc = "Debug: eval at cursor", }, { "<space>dE", function() require("dapui").eval(vim.fn.input("[Expression] > ")) end, desc = "Debug: eval expression", }, { "<space>d[[", function() require("dap").step_back() end, desc = "Debug: step back (1ステップ戻る)", }, { "<space>d]]", function() require("dap").step_over() end, desc = "Debug: step over (次のステップまで進める)", }, { "<space>d}}", function() require("dap").step_into() end, desc = "Debug: step into (関数の中へ)", }, { "<space>d{{", function() require("dap").step_out() end, desc = "Debug: step out (関数から外へ)", }, { "<space>dh", function() require("dap.ui.widgets").hover() end, desc = "Debug: hover", }, { "<space>dq", function() require("dap").terminate() end, desc = "Debug: terminate (デバッグの停止)", },}
Styluaでフォーマットされてしまうのが気になる場合はignoreで無視させましょう。
-- stylua: ignore start-- ここにコード-- stylua: ignore end
トラブルシューティング
遭遇しがちなトラブルと解決方法をまとめました。
まずはTroubleshootingのチェックリストで解決すると良さそうです。
ブレークポイントで止まらない
ブレークポイントを設定したにもかかわらず、止まってくれずに素どおりされるトラブルが起きた場合。
この場合、signcolumnはB
(ブレークポイント)ではなくR
(リジェクト=拒否)に変わっているでしょう。
これは「デバッグ向けのビルド」にしていないことが原因です。
詳しくはビルドの設定に書きました。
nvim-dap-uiのボタンが表示されない
nvim-dap-uiのボタンが表示されない場合、winbarの設定と競合しています。
筆者のケースではlualineでwinbarを上書きしてしまっていたのが原因でした。nvim-dap-uiのウィンドウでは、lualine側のwinbarの表示を無効化しました。
require("lualine").setup({ options = { disabled_filetypes = { winbar = { "dap-repl", }, }, }, -- ……})
無事ボタンが表示されています。

さらに見た目をスッキリさせたい場合、REPL以外のwinbarも無効化してしまいましょう。
require("lualine").setup({ options = { disabled_filetypes = { winbar = { "dap-repl", "dapui_breakpoints", "dapui_console", "dapui_scopes", "dapui_watches", "dapui_stacks", }, }, }, -- ……})
DAPのヘルプはいったいどこへ……?
:help dap
はパラグラフ削除のヘルプが開きます。
nvim-dapなら:help dap.txt
やAPI名でヘルプを開きましょう。
以上、NeovimでDAPを使ってデバッグする方法のまとめでした。
初見ではとっつきにくそうな感じでしたが、使ってみると便利です。