😽

nvim-dap の設定方法

2024/10/12に公開

nvim-dap の設定方法を覚えては忘れを繰り返しているので文章として残しておく。
この記事は nvim-dap v0.8.0 に基づいている

そもそも Debug Adapter Protocol (DAP) とは何かという詳細な説明は公式ページを参照
https://microsoft.github.io/debug-adapter-protocol/overview

基本

言語問わず知っておいたほうがいいのは doc のこの3箇所

例えばこのような設定を書いたとする

local dap = require 'dap'

dap.configurations.foo = {
  {
    name = "debug sample 1",
    type = "foo",
    request = "attach",
  },
  {
    name = "debug sample 2",
    type = "bar",
    request = "attach",
  },
}

dap.adapters.foo = {
  type = "server",
  host = "127.0.0.1",
  port = 4040,
}

dap.adapters.bar = {
  ...
}

このとき filetype foo を開いているときに nvim-dap を使うと

  • dap.configurations.foo の設定が読み出される
  • ユーザーが好みの設定を選ぶ
  • 例として1番目の設定 (name = "debug sample 1") を選んだとするとの typefoo なので dap.adapters.foo が使われる

という流れになる。同様に2番目の configurations を選択すると typebar のため dap.adapters.bar が使用される。

nvim-dap-xxx は何をしているのか?

nvim-dap-pythonnvim-dap-goといった nvim-dap の extension があるがこれらは一体何をしてくれるものなのだろうか?
始め私はこれらが各言語用の debug adapter なのだと思っていたがそうではなかった。あくまで dap.configuration.xxxdap.adapters.xxx を各言語用に設定してくれるものであって debug adapter そのものではないようだ。
そのため自力で書けるのであれば nvim-dap-xxx plugin を無理に使う必要はない。
configuration に独自のものを入れ始めると、今度は adapter に手を入れたくなってくるので次第に自分で extension を書き始めるという状況になりそうな気はする。

vscode-xxx を使っているものとそうでないものがあるのは何故か?

nvim-dap の各言語の adapter の設定方法を見ると debug adapter を VSCode 用の extension から拝借してくるように指示されたものとそうでないものがある。ものによっては両方ある。
debug adapter は vscode-xxx という名前がついていたとしても VSCode に特化したものでない限り VSCode でなくても使用することができる。Neovim なのに vscode-xxx を使えるのはそのため。
一方で Go の debugger である delve は adapter を介さず直接 DAP に対応している。このような場合、delve につなぐ設定を書くだけで nvim-dap との連携ができる。delve をインストールさえしておけば次のように書くだけで Go のプログラムを debug することができる。

local dap = require 'dap'

dap.configurations.go = {
  {
    {
      type = 'delve',
      name = 'Debug',
      request = 'launch',
      mode = 'debug',
      program = '${file}',
    },
  }
}

dap.adapters.delve = function(cb, cfg, parent)
  local uds = os.tmpname() .. '.sock'
  cb({
    type = 'pipe',
    pipe = uds,
    executable = {
      command = 'dlv',
      args = { 'dap', '-l', 'unix:' .. uds },
    },
  })
end

delve は DAP に対応しているし、rdbgGDB も DAP に対応しているので最近は個別の debug adapter を介さず debugger が DAP に歩みよるケースも珍しくないようである。

nvim-dap-xxx と vscode-xxx のどちらを使うかは個人の好みで決めて良いと思うが、すでに .vscode/launch.json がある場合はそのまま使える(可能性が高い) VSCode 用のものを使うと考えることが少なくて良いかもしれない。

設定方法

dap.adapters

dap.adapters テーブルの要素は debug adapter そのものではなく、adapter の起動方法や adapter につなぐための設定が書かれたテーブルまたは関数である。
渡された configuration の項目をもとに adapter の起動方法を変える場合など、複雑なことをしたい場合はテーブルより関数で定義すると良い。
前述のとおり dap.adapters の key 名は何でも良いので一つの言語を扱うときでも configuration の type を変えれば使用する adapter を変えることができる。

adapter の設定方法

テーブルの場合 type として executable, server, pipe のいずれかを指定する。debug adapter によってそれぞれ選択すべきものが異なるので適したものを選ぶ必要がある。
debug adapter との通信方法が stdio 経由なら executable、TCP 経由なら server、unix domain socket 経由なら pipe を選択する。

関数の場合は、function (callback, config, parent) という interface を満たした関数を定義する。callback にはテーブルだけで設定した場合と同じものを渡す。
例えば、以下の2つの書き方はどちらも同じ意味になる。

dap.adapters.foo = {
  type = 'server',
  host = '127.0.0.1',
  port = 8080,
}

dap.adapters.foo = function(callback, config, parent)
  callback({
    type = 'server',
    host = '127.0.0.1',
    port = 8080,
  })
end

config にはユーザーが選択した configuration が渡ってくる。adapter の設定は一つにして config の要素によって callback に渡す設定を分岐するといったこともできる。
実際に debug adapter に config を渡す前に config を編集したい場合は後述の enrich_config を使うことで実現できる。
parent は親の session が渡ってくるらしいのだが私は使ったことがないので実際のユースケースはよくわかっていない。

ref: https://github.com/mfussenegger/nvim-dap/blob/0.8.0/doc/dap.txt#L180-L183

enrich_config

どの adapter type でも使えるもので enrich_config という debug adapter にリクエストを投げる前に config を編集する機能がある。
全 configuration で共通して設定したいものなどは元の configuration の方に書くのではなく、enrich_config の時点で要素を追加・編集することで冗長性を減らすことができる。
コンパイルなどもこの enrich_config 関数のなかで実行することができるので debug に実行ファイルが必要な場合でも事前にコンパイルする手間を省くことができる。

dap.adapters.foo = {
  type = 'server',
  host = '127.0.0.1',
  port = 8080,
  enrich_config = function(config, on_config)
    local final_config = vim.deepcopy(config) -- 元の configuration の内容に副作用がないように deepcopy
    final_config.args = {'some', 'extra', 'args'}
    on_config(final_config)
  end
}

dap.configurations

configuration の必須項目は type, request, name でそれ以外の項目は基本的には各 debug adapter 依存である。
debug adapter が対応していない独自項目も追加可能でそれらは前途の adapter の処理の中で参照して独自動作の制御に使用することができる。

configuration の設定方法

request には launchattach のいずれかを指定する。
configuration を選択した際に debuggee (debug 対象のプログラム)を起動する際は launch を選択し、すでに起動している場合は attach を選択する。
ただし、configuration の厄介なところは、request に間違ったものを選択しても普通に debug できてしまうことがあることである。
debug ができるけれども引数が渡らないなどのときは request の種別が間違っていることを疑うと良い。

項目には関数も書くことができ nvim-dap-go では動的に args を取るためにこれが使用されている。
doc には thread を返す関数が書ける と書いてあるのだが、nvim-dap-python では vim.split の結果を返却する関数が定義されているため、thread を返す関数に現状は限定はされていないように見える。

debug adapter 依存の項目に関して何が設定できるのかわからないときは VSCode での設定方法を参照すると良い。
JSON を lua table に直せばだいたい動く。

.vscode/launch.json

load_launchjs 関数を使うことで VSCode の .vscode/launch.json を取り込むことができる。

require('dap.ext.vscode').load_launchjs(path, type_to_filetypes}))

第一引数は launch.json のへの path でデフォルト値は .vscode/launch.json である。
第二引数は launch.json に書かれている configuration をどの言語のものとして取り込むかの対応テーブルである。
第二引数を渡さなかった場合は、configuration に type を filetype とみなして取り込まれる。

{
    "type": "delve",
    "name": "Debug",
    "request": "launch",
    "mode": "debug",
    "program": "${file}"
}

上記のような設定が launch.json に書かれていた場合、delve は Go の debugger なので dap.configurations.go に設定が取り込まれてほしいが、第二引数を渡さないと dap.configurations.delve に取り込まれてしまう。
このようなことを回避するには以下のように type と filetype の紐付けを行う

require('dap.ext.vscode').load_launchjs(nil, { delve = {'go'}))

v0.8.0 の時点では人力で load_launchjs を使って読み込む必要があるが、現在の main branch だとファイルタイプ関係なく自動的に読み込まれるようになっているので次のリリースのタイミングではこの関数はもう使わなくて良くなると思われる。

各言語の設定例

Go

delve に直接接続する場合

-- remote debug でない debug の場合は dap-go に設定されたものを素直に使う
local dap = require 'dap'

require('dap-go').setup({
  dap_configurations = {
  },
  delve = {
    path = "dlv",
    initialize_timeout_sec = 20,
    port = "${port}", -- '${port}' と書いておくと nvim-dap 側が自動的に空いている port を使ってくれる
    args = {},
    build_flags = "",
  }
})

-- remote debug 用の configuration
table.insert(dap.configurations.go, {
  name = "Remote Debug",
  type = "remote_delve",
  request = "attach",
  mode = "remote",
  substitutePath = {
    {
      from = "/workspaces/nvim-dap-sample/go", -- host 側の path
      to = "/app"			                   -- remote 側の path
    },
  },
})

-- remote debug 用 adapter
dap.adapters.remote_delve = {
  type = "server",
  host = "127.0.0.1",
  port = 4040,
}

dap-go の dap_configurations に追加したい configuration を書くと dap.configurations.go に追加してくれるが、 type = "go" 以外のものは追加してくれないので remote debug 用の設定が欲しい場合は直接 table.insert で入れる必要がある。ちなみに今後も任意の type 対応はされない雰囲気を感じる

vscode-go を経由する場合

go の場合 vscode-go にこだわる必要はないが一応手順を残しておく。

vscode-go は mason.nvim を使っても入れられるが、mason が何やっているのかを知っておくという意味で今回は手動で入れていく。

cd ~/.local/share/${NVIM_APPNAME:-nvim}
VSCODE_GO_VERSION=0.42.1
wget https://github.com/golang/vscode-go/releases/download/v${VSCODE_GO_VERSION}/go-${VSCODE_GO_VERSION}.vsix
unzip go-${VSCODE_GO_VERSION}.vsix -d vscode-go

vscode-go を使った場合は adapter を remote debug 用とそうでないもので分ける必要はない。

local dap = require 'dap'

dap.adapters.vscode_go = function(callback, config)
  config.dlvToolPath = vim.fn.exepath('dlv')
  callback {
    type = "executable",
    command = "node",
    args = { vim.fn.stdpath('data') .. '/vscode-go/extension/dist/debugAdapter.js' }
  }
end

dap.configurations.go = dap.configurations.go or {}


vim.list_extend(dap.configurations.go, {
  {
    name = "vscode go",
    type = "vscode_go",
    request = "launch",
    program = "${file}",
  },
  {
    name = "remote debug vscode go",
    type = "vscode_go",
    request = "attach",
    mode = "remote",
    port = 4040,
    host = '127.0.0.1',
    apiVersion = 1,
    -- (remotePath, cmd) を設定するか substitute-path を設定する
    -- remotePath = "/app",
    -- cwd = "/workspaces/nvim-dap-sample/go",
    substitutePath = {
      {
        from = "/workspaces/nvim-dap-sample/go", -- ホスト側の path
        to = "/app"                              -- リモート側(docker とか) の path
      },
    },
  },
})

Ruby

nvim-dap の wiki でも nvim-dap-ruby でも rdbg と TCP で通信する設定になっているが rdbg は Unix Domain Socket (UDS) 経由にも対応しているので UDS を使う方式で書いてみるとこんな感じ

local dap = require 'dap'

dap.adapters.ruby = function(callback, config)
  callback {
    type = "pipe",
    pipe = "${pipe}", -- '${pipe}' とすると nvim-dap が自動的に pipe (socket) を作ってくれる
    executable = {
      command = "rdbg",
      args = { "--sock-path", "${pipe}", "--open", "--command", "--", config.command, config.script },
    }
  }
end

dap.configurations.ruby = {
  {
    type = "ruby",
    name = "debug current file",
    request = "launch",
    command = "ruby",
    script = "${file}",
  },
}

単一の Ruby ファイルであれば adapter の設定は executable で rdbg を起動するだけで普通に debug できると思うが、Rails だとおそらく失敗する。

rdbg 使って rails を起動すると以下のように debugger のログが2つ出る

$ rdbg --open --nonstop --command -- rails server
DEBUGGER: Debugger can attach via UNIX domain socket (/tmp/rdbg-1000/rdbg-30519)
DEBUGGER: Debugger can attach via UNIX domain socket (/tmp/rdbg-1000/rdbg-30519)
=> Booting Puma
=> Rails 7.1.4 application starting in development 
=> Run `bin/rails server --help` for more startup options
Puma starting in single mode...
* Puma version: 6.4.3 (ruby 3.3.5-p100) ("The Eagle of Durango")
*  Min threads: 5
*  Max threads: 5
*  Environment: development
*          PID: 30519
* Listening on http://127.0.0.1:3000
* Listening on http://[::1]:3000
Use Ctrl-C to stop

--nonstop オプションを外して実験してみると実際に2回 dap server が立ち上がっているらしいことがわかる。このうち後半の方に接続したいのだが nvim-dap は1回目の方を掴んでしまって失敗しているのではないかと想像している。

rdbg_rails

最近だと、rails new したときの初期設定は debugger.break などに到達したときに debugger が有効になるという設定になっているらしいのだが、それだと editor 側で設定した nvim-dap の breakpoint に反応してくれないので nvim-dap との相性は悪くなった気がする。
https://github.com/rails/rails/pull/51692

require: debug/prelude を外すとあとは debug adapter に接続しに行くタイミングをずらす adapter の設定を書けば debug できるものの、人力で立ち上げたあとに attach したほうが悩むこと少なくて楽そうではある。
非同期処理の練習がてら接続タイミングをずらした処理を書いてみたものの、書き方が正しいのかはよくわからない。ChatGPT が教えてくれたコードをつないでいったら動くことには動いた。
https://github.com/goropikari/nvim-dap-rdbg/blob/09a2d8abc5b91d3fb44211387c770e980b81cf6a/lua/dap-rdbg.lua#L96-L139

VSCode だと require: debug/prelude を外さずとも debugger が起動してきたが私は Ruby に精通していないのでどういう仕組みなのかはよくわかっていない。

C++

vscode-cpptools を使う場合

mason で入れられる cpptools は古いので人力で download してくる

cd ~/.local/share/nvim/
publisher=ms-vscode
extension_name=cpptools
version=1.22.6
targetPlatform=linux-x64
curl -L http://${publisher}.gallery.vsassets.io/_apis/public/gallery/publisher/${publisher}/extension/${extension_name}/${version}/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage?targetPlatform=${targetPlatform} -o cpptools.vsix
unzip -o -d cpptools cpptools.vsix
chmod +x ./cpptools/extension/debugAdapters/bin/OpenDebugAD7
local dap = require 'dap'

dap.adapters.cppdbg = {
  id = 'cppdbg',
  type = 'executable',
  command = vim.fn.stdpath('data') .. '/cpptools/extension/debugAdapters/bin/OpenDebugAD7',
  enrich_config = function(config, on_config)
    local final_config = vim.deepcopy(config)
    vim.fn.system({ 'g++', '-g', '-O0', vim.fn.expand('%'), '-o', vim.fn.expand('%:r') })
    on_config(final_config)
  end,
}

dap.configurations.cpp = {
  {
    name = 'Build and debug active file',
    type = 'cppdbg',
    request = 'launch',
    program = '${fileDirname}/${fileBasenameNoExtension}',
    cwd = '${fileDirname}',
  },
}

cpptools を使う際は事前にプログラムをコンパイルしておく必要があるので enrich_config を使って debug が始まる前にコンパイルを実行している。
ここでは簡単のために固定のコンパイル方法にしているが、configuration にコマンドを書いておき、それを enrich_config の中でとりだして使うということもできる。

事前にコンパイルする方法として下記のように configuration の項目に関数定義をすることでも実現できる。
ただし、.vscode/launch.json を流用したい場合はこの方法が通用しないので enrich_config にまとめると良いと思う。

{
  name = 'Build and debug active file',
  type = 'cppdbg',
  request = 'launch',
  program = '${fileDirname}/${fileBasenameNoExtension}',
  cwd = '${fileDirname}',
  prehook = function()
    vim.fn.system({ 'g++', '-g', '-O0', vim.fn.expand('%'), '-o', vim.fn.expand('%:r') })
  end,
},

GDB を直接使う場合

local dap = require 'dap'

dap.adapters.gdbdbg = {
  type = 'executable',
  command = 'gdb',
  args = { '-i', 'dap' },
  enrich_config = function(config, on_config)
    local final_config = vim.deepcopy(config)
    vim.fn.system({ 'g++', '-g', '-O0', vim.fn.expand('%'), '-o', vim.fn.expand('%:r') })
    on_config(final_config)
  end,
}

dap.configurations.cpp = {
  {
    name = 'Build and debug active file',
    type = 'cppdbg',
    request = 'launch',
    program = '${fileDirname}/${fileBasenameNoExtension}',
  },
}
GitHubで編集を提案

Discussion