Neovim上のLuaのコードをNeovimでデバッグ実行する

やりたいこと

やりたいことは、Neovimのプラグインやプラグインまでとはいかなくても設定した自作のコマンドのデバッグ実行です。

しかし…

普通のLuaのコードの場合であれば、普通のLuaのデバッガーを使えばデバッグが可能です。 mfussenegger/nvim-dapのドキュメントである https://codeberg.org/mfussenegger/nvim-dap/wiki/Debug-Adapter-installationを見てみると、EmmyLua/EmmyLuaDebuggertomblind/local-lua-debugger-vscode を使えばNeovimからデバッグ実行ができるようです。(筆者はやったことはありませんが…)

しかし、ここで考えているケースでは、Luaを実行するランタイムがNeovimに組み込まれているため、上記で述べた方法ではデバッグができません。

動機

こんなことをやりたいと思った動機は、プラグインのコードはイベント駆動で次々と実行されるコードが移動するため、LSPによる定義ジャンプだけではコードを読むのが大変だったからです。 ある程度大規模なプラグインでは、どこから読めばいいか分らないことも多々あります。

どの手法を使うか?

少しニッチなケースですが、地球上には同じようなことを考える(?)人間が存在するようで何人かの先駆者がいました。

日本語の記事としては以下が見つかりました。

この方法はVsCode向けのデバッガの実装を用いて、デバッグしていますが、個人的に難点だと思うのが、

  • Node.jsに依存するため環境構築が面倒くさい
  • git cloneしてから自分でnpm buildでビルドする必要がある
  • さらにLua on Neovimをインタプリンタとして実行する用のツールも必要

です。

ちなみにこの記事は以下の英語の記事を参考にして書かれています。

先程のnvim-dapのドキュメントにneovim-luaの項目があったので見てみると、 jbyuki/one-small-step-for-vimkindを使う方法が紹介されていました。

この方法は他に依存するソフトウェアは存在せず、Neovimのプラグインとしてインストールしてnvim-dapの設定に追加するだけで完了します。 そのため、筆者にはとても魅力的に感じこの方法を採用しました。

setup

jbyuki/one-small-step-for-vimkind/doc/osv.txtに詳細な設定ガイドがあるので,その通りの設定でokです。

インストール

jbyuki/one-small-step-for-vimkindは、nvim-dapの依存関係としてインストールします。 筆者はlazy.nvimを使用しているので、以下のように設定しました。

1
2
3
{ 'mfussenegger/nvim-dap', dependencies = {
  'jbyuki/one-small-step-for-vimkind',
}, lazy = false },

アダプタの設定

nvim-dapに生やす設定は以下です。 他の言語のアダブターの設定とほぼ同じなので、経験があれば特に困らないと思います。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
dap.configurations = {
--[[
他の言語向けの設定
--]]

  lua = {
    {
      type = 'nlua',
      request = 'attach',
      name = 'Attach to running Neovim instance',
    },
  },
}

dap.adapters.nlua = function(callback, config)
  callback({ type = 'server', host = config.host or '127.0.0.1', port = config.port or 8086 })
end

vim.keymap.set('n', '<leader>yl', function()
  require 'osv'.launch({ port = 8086 })
end, { noremap = true })

相違点としては、

1
2
3
vim.keymap.set('n', '<leader>yl', function()
  require 'osv'.launch({ port = 8086 })
end, { noremap = true })

で、デバッグされる側のNeovimを<leader>ylでサーバーとして起動するキーバインドくらいです(このキーバインドはこの記事の後半で使います!)。

その他解説上必要になりそうな設定

ブレークポイントの設定やデバッグの開始/停止などのnvim-dapの操作ですが、筆者は以下設定でキーバインド/コマンド化したので、参考までに以下に載せておきます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
local dap = require('dap')
-- commands & keymap
-- Setting breakpoints
vim.api.nvim_create_user_command('B', function()
  require('dap').toggle_breakpoint()
end, { nargs = 0 })

-- ref: ':help dap-user-commands', ':help dap-api'
-- clearn all breakpoints
vim.api.nvim_create_user_command('Bc', function()
  require('dap').clear_breakpoints()
end, { nargs = 0 })

-- Luanching debug session and resuming execution
vim.api.nvim_create_user_command('C', function()
  require('dap').continue()
end, { nargs = 0 })

-- relaunch current session
vim.api.nvim_create_user_command('Cr', function()
  require('dap').restart()
end, { nargs = 0 })

-- terminate current session
vim.api.nvim_create_user_command('Ct', function()
  require('dap').terminate()
end, { nargs = 0 })

vim.fn.sign_define('DapBreakpoint', { text = '🛑', texthl = '', linehl = '', numhl = '' })
vim.fn.sign_define('DapStopped', { text = '➡️', texthl = '', linehl = '', numhl = '' })

-- Stepping through code
-- ref: ':help dap-mapping', ':help dap-api'
vim.keymap.set('n', '<M-s>', function()
  require('dap').step_into()
end, { desc = 'Debug: Step Into' })

vim.keymap.set('n', '<M-n>', function()
  require('dap').step_over()
end, { desc = 'Debug: Step Over' })

vim.keymap.set('n', '<M-f>', function()
  require('dap').step_out()
end, { desc = 'Debug: Step Out' })

vim.keymap.set('n', '<M-u>', function()
  require('dap').up()
end, { desc = 'Debug: up' })

vim.keymap.set('n', '<M-d>', function()
  require('dap').down()
end, { desc = 'Debug: down' })

コマンド(キーバインド)と操作の対応は以下の表の通りです。

key function 操作
:B require(‘dap’).toggle_breakpoint() 現在のカーソル位置でのブレークポイントのトグル
:Bc require(‘dap’).clear_breakpoints() 設定したブレークポイントをすべてクリア
:C require(‘dap’).continue() 継続
:Cr require(‘dap’).restart() セッションの再起動
:Ct require(‘dap’).terminate() セッションの終了
Alt + n require(‘dap’).step_over() 次の行の実行で、関数呼び出しは実行するが、中には入らない
Alt + s require(‘dap’).step_into() 次の行の実行で、関数呼び出しがあったら、その中に入る
Alt + f require(‘dap’).step_out() 現在いる関数を最後まで実行して、呼び出し元に戻る

デバッグ実行を開始する

Luaのスクリプトをデバッグ実行する

まずはexample.luaを例としてデバッグを行なってみます。

1
2
3
4
5
6
7
print("start")

for i = 1, 10 do
	print("i " .. i)
end

print("end")

以降の説明では、目的のコードを実行しデバッグされる側をNeovim(A)、ブレークポイントを設定してコードを開いて読む側をNeovim(B)とします。

まず、Neovim (A)を起動して、<leader>ylによってデバッグされる側のサーバーを起動します。

次に、Neovim (B)を起動して、デバッグ実行したいコードを開いてブレークポイントを:Bによって設定し、:Cによってデバッグを開始します。 この時ブレークポイントとなる行の左側は’🛑‘に変りますがコードはまだ実行されていません。

最後に、Neovim (A)で:luafile ./example.luaを実行して、デバッグしたいスクリプトの実行を行います。
この時、Neovim (B) でデバッグ実行が開始され、左側の’🛑‘が’➡️’に変化します。

以降は、Alt + n等でデバッグ実行ができるはずです。

自作コマンドや、関数にした処理は?

Neovimの設定中の処理すなわち、$HOME/.config/nvim/init.luaにぶら下がっている設定中のスクリプトですが、 筆者は今のところ、Neovimの起動時の読み込み処理のデバッグ実行のやり方は分りません。

しかし、コマンドやグローバルな関数としてアクセス可能な場合では上の例で:luafileでスクリプトを実行するタイミングで,それらの実行を行うことでデバッグ可能です。

プラグインの場合は?

基本的には、自作コマンド等と同じですが、コードを開いてブレークポイントを貼るコードは、 実際にプラグインとして読み込まれるディレクトリ(lazy.nvimの場合多くは$HOME/.local/share/nvim/lazy/<plugin>)のものを開くことが必要です。

また、lazy.nvimの場合では、インストールするプラグインで以下のように、dir = <path>としてプラグインを置いたパスを指定できます。

1
2
3
4
5
{
  <repo>/<plugin name>,
  opts = {},
  dir = <path to plugin>,
}

そのため、コードの一部にロガーを仕込みたい場合や、プラグイン開発のようにコードの編集とデバッグと編集を繰り返す場合では、この方法が有用です。

感想

このことを調べた時は、一応できるがあまり使わなかな~と思っていました。 しかし、最近思い通りに動かないプラグインのコードを調べるのに大活躍しました。 今後もちょくちょく出番がありそうな気がします。

CC BY
Hugo で構築されています。
テーマ StackJimmy によって設計されています。