neovim + luaで閉じタグの補完まわりを良い感じにする

閉じタグ補完だけなら調べればいくらでも出てくるが、改行時に開いたり閉じタグの重複防止なんか考えだすと結構詰まるので。 まあプラグインとか使えば一発で解決するけど、vimmer心としては自分で使う物は自分で作りたくなるよね。

準備

クロージャのリスト(テーブル)、カーソル下の文字を取得する関数、あとin演算子的な何かとその他必要な物を用意する。
これらを前提とし、以後省略する。

local opts = { silent = true, expr = true }
local keymap = vim.keymap.set

local enclosures = {
    parentheses = { '(', ')', '()' },
    brackets = { '[', ']', '[]' },
    braces= { '{', '}', '{}' },
    chevrons = { '<', '>', '<>' },
}

local function get_pos(n)
    local col = api.nvim_win_get_cursor(0)[2]
    return string.sub(api.nvim_get_current_line(), col + n, col + 1)
end

local function inoperator(n, v)
    for _, f in pairs(enclosures) do
        if f[n] == data then
            return true
        end
    end
end

閉じタグを補完する

luaでキーマップを設定する時はvim.keymap.setを使う。
基本はこれ。

keymap('i', '(', '()<left>', { silent=true })

for等で書き直す。luaの関数を使うので、expr=trueを追加する。
すくなくとも自分の環境では<left>が使えないので<esc>iを使う。

for _, f in pairs(enclosures) do
    keymap('i', f[1], '\'' .. f[3] .. '<esc>i\'', opts)
end

機能の近い物

対応する閉じタグをまとめて消す

中身が空の時だけやってくれれば良いので、カーソル下から2文字取得して分岐する。
このための要素をテーブルの3つめに入れてあるので、これを参照する。0-indexedぽい。

local function remover()
    local cp = get_pos(0)
    if inoperator(3, cp) == true then
        return '<bs><del>'
    else
        return '<bs>'
    end
end

keymap('i', '<bs>', remover, opts)

クロージャの中で改行した時に良い感じに開く

先程の関数と同じ事をして、戻り値を改行等にするだけ。
これについて調べると二回改行して上行ってインデントというパターンが多く見られるが、改行してインサートを抜けて<S-o>で良い。
ドットリピートに影響するので、それが問題になる場合は戻り値を替える。

local function indenter()
    local cp = get_pos(0)
    if inoperator(3, cp) == true then
        return '\n<esc><s-o>'
    else
        return '\n'
    end
end

keymap('i', '<return>', indenter, opts)

クロージャ内で空白を入力した時に二つ入れる

これも同じ方法でできるのでついでに加えておく。

local function spacer()
    local cp = get_pos(0)
    if inoperator(3, cp) == true then
        return '<space><space><left>'
    else
        return '<space>'
    end
end

keymap('i', '<space>', spacer, opts)

上の三つをまとめる

さすがに長いので一つにまとめる。
とりあえずテーブルを作ってforに入れれば動く。

local key = {
    bs = { '<bs>', '<bs><del>' },
    rt = { '<return>', '\n<esc><s-o>' },
    sp = { '<space>', '<space><space><left>' },
}

for _, f in pairs(key) do
    local function closing()
        local cp = get_pos(0)
        if inoperator(3, cp) == true then
            return f[2]
        else
            return f[1]
        end
    end
    keymap('i', f[1], closing, opts)
end

閉じタグの重複を避ける

補完してくれるのは嬉しいが、たとえば引数を受け取らない関数を書く時に())となったりして鬱陶しい。
そうでなくても、右キーは遠いしインサート抜けてlとかするよりは手動で入力してしまった方が早い。
これを解決するために、閉じタグを入力した時カーソル下に同じタグがあった場合に<right>を入力したい。

for _, f in pairs(enclosures)do
    local function closure()
        local cp = get_pos(1)
        if cp == f[2] then
            return '<right>'
        else
            return f[2]
        end
    end
    keymap('i', f[2], closure, opts)
end

雑だしもっと良い方法がありそう、でも動いてるのでひとまずは良し。