LuaでNeovimのプラグインを作ってみる際の試行錯誤
一度 Lua で Neovim のプラグインを作ってみようということで。
作りたいもの
ノーマルモードの <C-a>
や <C-x>
コマンド(数値のインクリメント・デクリメント)を拡張し、日付や曜日、 Markdown のセクション見出しなど、様々な数値を増減できるようにするプラグイン。
プラグインづくりほぼほぼ初めて + Luaについては完全に初心者なので、そこまで実装が難しくなさそうで、なおかつある程度実用性のありそうな(前からほしいと思っていた)ものを選びました。
類似プラグインとして vim-speeddating があります。
作業場所
考えている仕様
基本的にはVimの仕様と同様に、カーソル下にある数字 or 同じ行でカーソルより右側にある《数字》を、決まった《数字》だけインクリメント・デクリメントする、という動作。
ややこしいので前者の数字を 被加数 (augend)、後者の数字を 加数 (addend) と呼ぶことにする。
たとえばバッファ上に
hogehoge| fugafuga 123 piyo
とあったときに(|
はカーソル) <C-a>
を押すと
hogehoge fugafuga 12|4 piyo
となる。被加数はバッファ上に置かれた数値であり、加数は上の例では1である(3<C-a>
などのようにカウンタで指定可能にする)。
被加数の種類
<C-a>
とかでインクリメントできたら便利そうだな―と今考えているもの。上に行くほど優先順位が高い。
- 十進表記の整数(Vim/Neovimに機能あり)
- 十進表記の非負整数(
-
の影響を受けない。個人的に欲しい) - 日付
- 時刻
- 曜日
- 16進数表記の非負整数
- Markdown のヘッダの
###
の数
参考になりそうなリンク
基本的に日本語翻訳版があるものは日本語のほうを貼っている。
Lua + Neovim
Lua 単体
(Neo)vim
関連プラグイン
How to write neovim plugins in Luaを参考にして、以下のように書いてみた。
プラグイン名は dial.nvim
とした。
plugin/dial.vim
if exists('g:loaded_dial') | finish | endif " prevent loading file twice
let s:save_cpo = &cpo " save user coptions
set cpo&vim " reset them to defaults
" command to run our plugin
nnoremap <expr> <Plug>(dial-increment) '<Cmd>lua require"dial".increment(' .. v:count1 .. ')<CR>'
nnoremap <expr> <Plug>(dial-decrement) '<Cmd>lua require"dial".increment(' .. -v:count1 .. ')<CR>'
let &cpo = s:save_cpo " and restore after
unlet s:save_cpo
let g:loaded_dial = 1
lua/dial.lua
local function dbg(...)
print(vim.inspect(...))
end
local function increment(addend)
-- インクリメントする。
local bufnum, lnum, col, off, curswant = vim.call('getcurpos')
local line = vim.fn.getline('.')
local newline = line .. 'a'
local newcol = #newline
vim.fn.setline('.', newline)
vim.fn.setpos('.', {bufnum, lnum, col, off})
end
return {
increment = increment
}
この状態で適当に nmap <C-a> <Plug>(dial-increment)
などマッピングすると上の機能が使えるようになる。
今のところは「カーソルのある行の末尾に a
を追加する」だけの機能となっている。
lua/dial.lua
のところは、本当なら setpos
の効果も確かめたくて
local newline = line .. 'a'
local newcol = #newline
vim.fn.setline('.', newline)
vim.fn.setpos('.', {bufnum, lnum, newcol, off, curswant})
としたかったのだが、なぜかこうすると
E5100: Cannot convert given lua table: table should either have a sequence of positive integer keys or contain only string keys
E5108: Error executing lua error converting argument 2
というエラーが出ててしまう。 newcol
はただの数値のはずなので {bufnum, lnum, newcol, off, curswant}
もただの数値の配列になるはずだし、 setpos
の引数の要件も満たしているはずなのだが。
原因が分かった。
local bufnum, lnum, col, off, curswant = vim.call('getcurpos')
というところで、正しく bufnum
などの変数に値が格納できていなかった。
というのも vim.call('getcurpos')
が返すのは「複数の戻り値」ではなく「1つのテーブル(テーブルの中に5つの値が入ってる)」であり、正しくは
local retval = vim.call('getcurpos')
local bufnum = retval[1]
...
としなければならなかった。
しかしそれなら local bufnum, lnum, col, off, curswant = vim.call('getcurpos')
の時点でエラーを吐いてほしいし、vim.inspect(bufnum, lnum, col, off, curswant)
したときに
{..., ..., ..., ..., ...}
ではなく {..., ..., ..., ..., ...}, nil, nil, nil, nil
と返ってきて欲しい。
おかげで原因特定にやたらと時間がかかってしまった。
dbg({bufnum, lnum, newcol, off, curswant})
を見て
{ { 0, 15, 5, 0, 5 },
[3] = 1
}
と出てきてはじめてこのことに気づいた。
nil
に対して妙に寛容なLuaの言語仕様、エラーの原因を特定しづらくしているだけのような気がする。
とりあえず簡単に「十進数の整数」を increment/decrement する機能を追加。
lua/dial.lua
local function dbg(...)
print(vim.inspect(...))
end
-- cursor 上にあるか、もしくは cursor の後にある数字を検索する。
local function search_decimal(line, cursor)
local idx_start = 1
local s, e
while idx_start < #line do
s, e = line:find("-?%d+", idx_start)
if s then
-- 検索結果が見つかれば
if (cursor <= e) then -- cursor が終了文字より手前にあればそれが答え
return s, e, string.sub(line, s, e)
else
idx_start = e + 1
end
else
-- 検索結果がなければ nil を return
break
end
end
return nil
end
-- インクリメントする。
local function increment(addend)
-- 現在のカーソル位置、カーソルのある行の取得
local curpos = vim.call('getcurpos')
local col_cursor = curpos[3]
local line = vim.fn.getline('.')
-- 数字の検索、加算後のテキストの作成
local s, e, text = search_decimal(line, col_cursor)
if s == nil then
return
end
local num = tonumber(text)
local newnum = num + addend
local newtext = tostring(newnum)
local newline = string.sub(line, 1, s - 1) .. newtext .. string.sub(line, e + 1)
local newcol = s - 1 + #newtext -- newtext の末尾に持ってくる
-- 行編集、カーソル位置のアップデート
vim.fn.setline('.', newline)
vim.fn.setpos('.', {curpos[1], curpos[2], newcol, curpos[4], curpos[5]})
end
return {
increment = increment
}
これから日付や曜日といったパターンを追加していくが、そのためにもう少し構造化したい。たとえば数値と日付は格納すべきデータ型も異なるものの、
- 文字列の中から該当するパターンを検索する
- マッチした文字列から必要な情報(具体的な日付など)を取得し、具体的な値をデータに格納する
- 足し算処理を行い、データを更新する
- 更新後のデータを文字列に埋め込む
という処理は共通している。クラスもしくはインターフェースとしてこれらの振る舞いを記述したい。
Rustでいうとこんな感じ。
trait Augend {
fn find(line: &str, cursor: usize) -> Option<Self>;
fn add(&mut self, addend: u64);
fn to_string(&self) -> String;
}
型をベースに設計を決めてる時点で、あんまり Lua っぽくない開発の仕方をしている気がする…
開発過程をスクラップにするのを完全にサボってしまったものの、なんとか形にはなった。
プラグインの紹介記事書いたらこのスクラップを閉じよう。