Neovimでバッファを縞模様にする

2 min read読了の目安(約2100字

バッファを縞模様にハイライトするプラグインを作成した。

https://github.com/notomo/zebrazone.nvim

当然これには実用性はなく、nvim_set_decoration_provider()で遊ぶのが目的。
最近このAPIを触っていてわかったことをまとめる。

nvim_set_decoration_provider()で何を解決できる?

前提として、バッファをハイライトする方法には大まかに以下がある。

  • 正規表現での指定 (:syntax, matchadd())
  • 位置での指定 (matchaddpos(), nvim_buf_set_extmark()など)

簡単な正規表現で済む場合は:syntaxでいい。
しかし、バッファ上のテキストに依らず保持するデータを元にハイライトするのは難しい。
位置を指定する関数の場合はそれが可能。
しかし、行ごとにハイライトする場合は行が増えるほどコストが増える。
表示範囲だけを処理したいがautocmdでの実現はめんどくさい。

この問題を解決できる。

コード例

以下は行数に応じて決まったハイライトグループでハイライトする例。

local Highlighter = {}
Highlighter.__index = Highlighter

function Highlighter.new(bufnr, ns)
  local tbl = {_bufnr = bufnr, _ns = ns}
  return setmetatable(tbl, Highlighter)
end

local hl_groups = {"DiffAdd", "DiffChange", "DiffDelete"}
function Highlighter.highlight(self, row)
  vim.api.nvim_buf_set_extmark(self._bufnr, self._ns, row, 0, {
    end_line = row + 1,
    hl_group = hl_groups[(row % #hl_groups) + 1],
    hl_eol = true,
    ephemeral = true,
  })
end

local highlighters = {}
local ns = vim.api.nvim_create_namespace("example")
vim.api.nvim_set_decoration_provider(ns, {
  on_win = function(_, _, bufnr)
    return highlighters[bufnr] ~= nil
  end,
  on_line = function(_, _, bufnr, row)
    highlighters[bufnr]:highlight(row)
    return true
  end,
})

local bufnr = vim.api.nvim_create_buf(false, true)
vim.cmd("vsplit | buffer " .. bufnr)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, vim.fn["repeat"]({""}, 100000))
highlighters[bufnr] = Highlighter.new(bufnr, ns)

重要なのはnvim_buf_set_extmark()のオプションにephemeral = trueを渡している点。
ephemeralなextmarkは再描画されるまでの間のみ描画され、
nvim_set_decoration_provider()on_lineコールバックはその行が再描画される度に呼ばれる。

バッファに10万行あっても初回ハイライトのコストはウィンドウの表示範囲分だけで済む。
逆に言うと再描画の度にコストを払うので、重い処理を実行するべきではない。
(この辺りはヘルプに書いてある)

感想

プラグインの提供するバッファのハイライトに便利なAPIだった。
元々treesitterでのパース結果を用いたハイライトに使われてるAPIだが、
アイデア次第でもっと色々とできそう。

例えば、ファジーファインダーのマッチ位置のハイライトに使えることは確認した。
(件数が多いと結局は絞り込みに時間がかかるので大して速くならなかったが実装は楽になった)