Neovimのマウスジェスチャーpluginで線を描く
マウスジェスチャーを可能にするプラグイン gesture.nvim を作っている。
以前は実現できなかった楽しい機能を追加した。
それがこちら。
今回はこの機能の実装に使ったテクニックを紹介する。
透明なfloating window上でのhighlight
winblend
を使うとfloating windowを(擬似的に)透明にできる。
以下でwinblend=100
のfloating windowを表示し、そのウィンドウ全体を動けるようにする。
元のウィンドウ、バッファに影響を与えずに装飾できるのが利点。
local bufnr = vim.api.nvim_create_buf(false, true)
local height = vim.o.lines
local window_id = vim.api.nvim_open_win(bufnr, true, {
width = vim.o.columns,
height = height,
relative = "editor",
row = 0,
col = 0,
external = false,
style = "minimal",
})
vim.wo[window_id].winblend = 100
local lines = vim.fn["repeat"]({""}, height)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
vim.o.virtualedit = "all" -- 実際はジェスチャー終了時に元の値に復元する
このウィンドウ上で目に見えるhighlight groupを作るにはblendを100より小さくする必要がある。
例えば、以下で線を不透明な白にできる。
highlight! GestureLine guibg=#ffffff blend=0
extmarkで線を描画する
これはテクニックと言えるか怪しいが、
1行を1個のextmarkで表現すると大量に描画してもパフォーマンスが落ちにくい。
ざっくり1行を描画する部分をコードにするとこうなる。
local line = lines[row] or {}
local ranges = line.ranges or {}
local virt_text = to_virt_text(ranges)
local id = vim.api.nvim_buf_set_extmark(bufnr, ns, row - 1, 0, {virt_text = virt_text, id = line.id})
lines[row] = {id = id, ranges = ranges}
既にセットしてあるextmarkを編集できるので、描画ごとにクリアしないで済む。
描画範囲をvirt_textに渡せる形式にするのは愚直にやるのみ。
{{1, 2, "GestureLine"}, {4, 5, "GestureLine"}}
を
{{" ", "GestureLine"}, {" "}, {" ", "GestureLine"}}
にする。
実際のコードは方向を表示するレイヤーを作るためにもっと苦しい。
スクロール禁止(仮)
ただのウィンドウなので、マウスホイールでスクロールすると描画が上に吹っ飛んでいく。
これを完全に阻止する方法は見つけられていない。
ただ、スクロール後に元に戻す方法はあった。
nvim_set_decoration_provider()
を使うと、以下のように描画にフックした処理を書ける。
vim.api.nvim_set_decoration_provider(ns, {
on_win = function(_, _, buf, topline)
if topline == 0 or buf ~= bufnr or not vim.api.nvim_win_is_valid(window_id) then
return false
end
vim.fn.winrestview({topline = 0, leftcol = 0})
end,
})
ヘルプにもあるが、主に描画ごとに処理したい一時的な(ephemeral) extmarkを扱うためのAPIで、
Neovim本体のtree-sitter highlighterの実装に使われている。
ちなみに同じnamespaceで上書きすると無効にできる。
vim.api.nvim_set_decoration_provider(ns, {})
感想
自分は使わなそうだと思っていたAPIにも意外な用途があって楽しい。
Discussion