🏃

誰でも簡単手作りわくわくオペレータ作成Neovimプラグイン

2024/03/20に公開

はじめに

Vim や Neovim には オペレータ と呼ばれるコマンドがあります。オペレータコマンドの直後に適用範囲を指定することで、様々な編集操作を実現できるというものです。

オペレータを用いると、たとえば以下のようなことができます。

  • yt" と打つと、カーソルからその右の " に到達するまでの範囲をヤンク(コピー)する
    • y: ヤンクを表すオペレータ
    • t": カーソル直後に存在する " の直前まで移動するモーション
  • >ip と打つと、カーソルのある段落のインデントを1段階増やす(右にずらす)
    • >: インデントを1段階増やすオペレータ
    • ip: 1つの段落を表すテキストオブジェクト

オペレータは間違いなく Vim を Vim たらしめる機能の一つであり、多くの Vimmer から愛されています。オペレータを用いた編集操作のどこに魅力があるのでしょうか。私は大きく分けて3つあると考えています。

  1. 掛け算の要領でできることが増えていく。

    • 10種類のオペレータと 20種類のモーション・テキストオブジェクトがあれば、単純計算で 200 通りの組み合わせ方があります[1]
    • 20種類のモーション・テキストオブジェクトを覚えた状態で1つ使えるオペレータが増えれば、それだけで20通りの操作が効率的に行えるようになるということです。
  2. ドットリピートできる。

    • . コマンドは直前に行った編集操作を繰り返すシンプルなものです。
    • カーソル位置が変われば適用範囲も変わるため、非常に汎用性が高いです。
    • たとえば、cgn(置換コマンド c + 検索にマッチする場所を表す gn)とドットリピートは特に強力なコンボであることが知られています(紹介例)。
  3. 拡張可能である。

    • Vim にはユーザ定義関数をオペレータ化する仕組みがあり、それを活用したプラグインがいくつも公開されています。
    • 代表的なのは vim-sandwich など、対象範囲を括弧で囲むオペレータでしょう。単語や段落など様々な対象を任意の括弧で囲め、さらにドットリピートまでできるのは非常に強力です。

Vim 標準で用意されている主要なオペレータは以下の通りです。削除やインデントに限らず、様々な操作ができることが分かりますね。これ以外にもまだあります (:h operator)。

オペレータ 機能
d 対象を削除する
c 対象を別のテキストで置き換える
y 対象をヤンク(コピー)する
> 対象行のインデントを1増やす
< 対象行のインデントを1減らす
= 対象行のインデントを整える
gU 対象を大文字にする
gu 対象を小文字にする
! 対象行に対し外部コマンドを実行する

しかし、私達が日々直面するテキスト編集はこれだけではありません。たとえば

  • 特定の base64 形式の文字列をデコードする
  • 特定の段落を英語に翻訳する
  • 特定の計算式を評価し、答えに変換する

といったものもある意味テキスト編集(変換)です。翻訳などは対応するプラグインがあるかもしれませんが、自分しか使わないようなニッチな変換を行いたいとなると、対応プラグインも限られてきます。そういったありとあらゆる変換をオペレータにすれば、テキスト編集効率を極限まで引き上げられるでしょう。

general_converter.nvim

というわけで、簡単にオペレータが作れる Neovim 用のプラグインを作りました。

https://github.com/monaqa/general-converter.nvim

コンセプトは誰でも簡単にオペレータが作れること。このプラグインを使うと、「文字列を受け取り文字列を返す関数」を与えるだけで、その編集操作をオペレータ化できます。
たとえば以下のような設定を書けば選択範囲を ( ) で囲むオペレータができます。

vim.keymap.set(
    { "n", "x" },
    "<Leader>)",
    require("general_converter").operator_convert(function(s)
        return "(" .. s .. ")"
    end),
    { expr = true }
)

<text>(<text>) に変換するような関数を与えるだけ。お手軽ですね!

活用例

特定の段落を英語に翻訳する

翻訳機能は何かしらプラグインと連携するのが最も楽でしょう。ここでは deepl.vim というプラグインを使ってみます。

https://github.com/ryicoh/deepl.vim

このプラグイン自体は 2024/03/20 時点でオペレータを提供していませんが、文字列を別の言語に翻訳するdeepl#translate() という関数があるため、 general_converter.nvim で簡単にオペレータを定義できます。

vim.keymap.set(
    "n",
    "@e",
    require("general_converter").operator_convert(function(s)
        return vim.fn["deepl#translate"](s, "en")
    end),
    { expr = true }
)

これで @eip とすれば特定の段落を英語に翻訳できます。もちろんドットリピートもできます。

特定の計算式を Vim script の式とみなして評価する

vim.api.nvim_eval を用いれば、任意の Vim script の式を評価できます。

vim.keymap.set(
    { "n", "x" },
    "@c",
    require("general_converter").operator_convert(function(s)
        return vim.fn.string(vim.api.nvim_eval(s))
    end),
    { expr = true }
)

Tips

setup 時に converter を登録しておけば、どのオペレータを実行するか vim.ui.select で選択できるようになります。

require("general_converter").setup {
    converters = {
        {
            desc = "Vim script の式とみなして計算する (1 + 1 -> 2, 40 * 3 -> 120)",
            converter = function(s)
                return vim.fn.string(vim.api.nvim_eval(s))
            end,
        },
        {
            desc = "英語に翻訳する",
            converter = function(s)
                return vim.fn["deepl#translate"](s, "en")
            end,
            labels = { "translate" },
        },
        {
            desc = "日本語に翻訳する",
            converter = function(s)
                return vim.fn["deepl#translate"](s, "ja")
            end,
            labels = { "translate" },
        },
    },
}

キーマッピング定義時に operator_convert() の引数を空にすると、 setup で定義した全ての編集操作が vim.ui.select の選択画面で表示され、選んだものが適用されます。

vim.keymap.set(
    { "n", "x" },
    "gc",
    require("general_converter").operator_convert(),
    { expr = true }
)


vim.ui.input で編集操作を選択する画面の例

operator_convert() の引数に文字列を与えると、setup で定義したオペレータのうちその文字列をラベルに持つような編集操作が選択画面に表示されます。

vim.keymap.set(
    { "n", "x" },
    "gc",
    require("general_converter").operator_convert("translate"),
    { expr = true }
)

いずれのキーマップも直前に選択した編集操作を記憶しているため、ドットリピートでは選択の手間は生じず、直前に実行したものと同じ編集操作がそのまま実行されます。たまにしか使わないオペレータをまとめて定義しておけば、覚えなければならないキーマップを減らせて便利です。

おわりに

冒頭で述べた通り、任意の関数をオペレータすること自体はプラグインがなくとも可能です。また、オペレータ作成をサポートする Vim script 製のプラグインも昔からあります (vim-operator-user) 。今回は Lua の言語仕様を活かし、よりシンプルなインターフェースを持つプラグインを自分で作った次第です。無名関数が簡単に扱えるのは Lua の良い所ですね。

今回は具体例としては取り上げませんでしたが、大規模言語モデル (LLM) が発展しつつある現在、より汎用的な処理が LLM に任せられるようになりました。コードのリファクタやテキストの要約などをオペレータにしてみても面白いかもしれませんね。みなさんも general_converter.nvim を使って、誰もがわくわくするようなオペレータを作ってみてください!

脚注
  1. 行単位で適用されるオペレータなどもあるため、実際に役立つ範囲で言えばそこまでのバリエーションはありません。とはいえ、オペレータ・モーション・テキストオブジェクトの引き出しが増えるにつれ、できることが加速度的に増えていくのは確かです。 ↩︎

Discussion