Lua で ddu.vim のカスタムアクションを実装する

前置き

Neovim のプラグインの ddu.vim を導入してバッファ切り替えやコマンド履歴からのコマンド実行やファイラーとして便利に使っているのですが、ddu.vimのアクション周りを便利にしよう という記事を見てカスタムアクションを導入したいと思いました。

ただ、上記の記事は Vimscript を使って設定していますが、私は Lua で設定していますので、導入では少々苦労しました。

ネットで調べても Lua でカスタムアクションを導入している記事は見当りませんでしたので、備忘録として注意点をメモします。

環境

OS

1エディション	Windows 11 Pro
2バージョン	23H2
3インストール日	2022/07/11
4OS ビルド	22631.4108
5エクスペリエンス	Windows Feature Experience Pack 1000.22700.1034.0

Neovim

1 nvim --version
2NVIM v0.10.1
3Build type: Release
4LuaJIT 2.1.1713484068
5Run "nvim -V1 -v" for more info

また、Neovim には ddu-source-action を追加しており、ddu.vim の UI で a を押せばi temAction として実行できるアクションをdduに表示して絞り込みができるようにしています。

1-- 無関係の設定は省略しています
2vim.api.nvim_create_autocmd("FileType", {
3  pattern = "ddu-ff",
4  callback = function()
5    vim.keymap.set({ "n", "i" }, "a", [[<Cmd>call ddu#ui#do_action('chooseAction')<CR>]], { noremap = true, silent = true, buffer = true })
6  end,
7})

実装

私が導入したいカスタムアクションは、ファイラーとして使っているときに、選択したファイルの相対パスをカーソル位置に挿入するというものです。

そこで、まず前述の記事に「まず登録する関数の先頭で試しにechomsg a:args等を実行して、受け取る引数に何が入っているかを確かめてみると良い」とありましたので、関数と関数呼び出しを以下のとおり設定して受け取る引数の内容を確認しようとしました。

1function InsertFilepath()
2  vim.cmd('echomsg a:args')
3  return 0
4end
5vim.fn['ddu#custom#action']('source', 'file_rec', 'insertPath', 'InsertFilepath')

これで chooseAction で呼び出したアクションの中に insertPath が登録されたのですが、アクションを実行しても「未知の関数です」というエラーになってしまいました。前述の記事と同様に VimScript で設定を書いて vim.cmd() で囲めばエラーにはならないのですが、設定は可能な限り Lua で書くようにしているので、他の方法も模索してみました。

で、結論を書きますと、関数を別途定義して呼び出すのではなく、即時関数にすればきちんとアクションを実行できました。それを踏まえて書いた設定は以下のとおりです。

1vim.fn['ddu#custom#action']('source', 'file_rec', 'insertPath', function (args)
2  local selectedFilePath = vim.fn.substitute("."..args["items"][1]["word"], "\\", "/", 'g')
3  local beforeCursor = vim.fn.strcharpart(vim.fn.getline('.'), 0, vim.fn.getcharpos('.')[3])
4  local afterCursor = vim.fn.strcharpart(vim.fn.getline('.'), vim.fn.getcharpos('.')[3], vim.fn.strchars(vim.fn.getline('.')))
5  local newLine = beforeCursor..selectedFilePath..afterCursor
6  vim.fn.setline('.', newLine)
7  return 0
8end)

実際の動作は次のとおりです。

補足

args の内容

chooseAction からカスタムアクションを呼び出したときに渡される引数の args について、以下のコードで内容をレジスタに登録してから別のバッファに貼り付けて確認したところ、以下の内容となっていました。

1vim.fn['ddu#custom#action']('source', 'file_rec', 'insertPath', function (args)
2  local selectedFilePath = vim.fn.substitute("."..args["items"][1]["word"], "\\", "/", 'g')
3  local beforeCursor = vim.fn.strcharpart(vim.fn.getline('.'), 0, vim.fn.getcharpos('.')[3])
4  local afterCursor = vim.fn.strcharpart(vim.fn.getline('.'), vim.fn.getcharpos('.')[3], vim.fn.strchars(vim.fn.getline('.')))
5  local newLine = beforeCursor..selectedFilePath..afterCursor
6  vim.fn.setline('.', newLine)
7  vim.fn.setreg('a', vim.inspect(args)) -- 確認のために追加したコード
8  return 0
9end)
  1{
  2  actionParams = vim.empty_dict(),
  3  context = {
  4    bufName = "C:\\Users\\username\\AppData\\Local\\nvim\\init.lua",
  5    bufNr = 3,
  6    cwd = "C:\\Users\\username\\AppData\\Local\\nvim",
  7    done = true,
  8    doneUi = true,
  9    input = "",
 10    maxItems = 111,
 11    mode = "n",
 12    path = "C:\\Users\\username\\AppData\\Local\\nvim",
 13    pathHistories = {},
 14    winId = 1000
 15  },
 16  items = { {
 17      __columnTexts = vim.empty_dict(),
 18      __expanded = false,
 19      __groupedPath = "",
 20      __level = 0,
 21      __sourceIndex = 0,
 22      __sourceName = { "file_rec" },
 23      action = {
 24        isDirectory = false,
 25        path = "C:\\Users\\username\\AppData\\Local\\nvim\\lua\\plugins\\cmp-path.lua"
 26      },
 27      display = " lua\\plugins\\cmp-path.lua",
 28      highlights = { {
 29          col = 1,
 30          hl_group = "DduDevIconLua",
 31          name = "ddu_devicon",
 32          width = 3
 33        } },
 34      kind = "file",
 35      matcherKey = "lua\\plugins\\cmp-path.lua",
 36      word = "lua\\plugins\\cmp-path.lua"
 37    } },
 38  options = {
 39    actionOptions = vim.empty_dict(),
 40    actionParams = vim.empty_dict(),
 41    actions = {},
 42    columnOptions = vim.empty_dict(),
 43    columnParams = vim.empty_dict(),
 44    expandInput = false,
 45    filterOptions = vim.empty_dict(),
 46    filterParams = vim.empty_dict(),
 47    input = "",
 48    kindOptions = {
 49      action = {
 50        defaultAction = "do"
 51      },
 52      file = {
 53        defaultAction = "open"
 54      }
 55    },
 56    kindParams = vim.empty_dict(),
 57    name = "file_recursive",
 58    postFilters = {},
 59    profile = false,
 60    push = false,
 61    refresh = false,
 62    resume = false,
 63    searchPath = "",
 64    sourceOptions = {
 65      _ = {
 66        ignoreCase = true,
 67        matchers = { "matcher_substring" }
 68      },
 69      file_rec = {
 70        actions = {
 71          insertPath = "e91b97c2196bc99e62c0f12111750920802d8bf4aea17d68a81a85fb5ffef268"
 72        }
 73      }
 74    },
 75    sourceParams = vim.empty_dict(),
 76    sources = { {
 77        name = { "file_rec" },
 78        options = {
 79          converters = { "converter_devicon" }
 80        },
 81        params = {
 82          ignoredDirectories = { "node_modules", ".git", "dist", ".vscode" }
 83        }
 84      } },
 85    sync = false,
 86    ui = "ff",
 87    uiOptions = vim.empty_dict(),
 88    uiParams = {
 89      ff = {
 90        floatingBorder = "rounded",
 91        previewFloating = true,
 92        previewFloatingBorder = "rounded",
 93        previewFloatingTitle = "Preview",
 94        previewSplit = "vertical",
 95        prompt = "> ",
 96        split = "floating"
 97      }
 98    },
 99    unique = false
100  }
101}

カスタムアクションを追加したかった理由

ddc.vim を導入して LSP やコマンドやファイルパスの補完を実現していますが、ファイルパスの補完だけちょっと動作にもたつきがあるのと、ディレクトリ構造を順番に入力するのがちょっと面倒だったためです。

それでも他に方法がなければ ddc.vim の補完を使うのですが、ddu.vim もファイルパスを使っている以上、カスタムアクションが導入できればファイルパスを一発で書けるのではないかと思い、カスタムアクションの導入に挑戦しました。

結果的に希望どおりの動作が実現できたので良かったです。