Neovimのテキストオブジェクトをカスタムできるmini.aiが便利

by
カテゴリ:
タグ:

Mini.aiについて

VimやNeovimのテキストオブジェクト、便利ですよね。

yi(すれば、括弧の中のどこにいても括弧の中全体をヤンクできると知った時は感動ものでした。

たとえば以下の|がカーソル位置ならyi(hoge fuga piyoをヤンクできます。 ya(にすれば括弧も含めてヤンクできます。

(hoge fug|a piyo)

エキストオブジェクトの便利さに関しては日本語の記事も沢山あるので、検索していただくとして、本記事では自分でテキストオブジェクトを定義する方法として、mini.aiプラグインを紹介します。

  • Lua製なので、init.luaを書いてる人と親和性が高い − テキストオブジェクトを自作できる
    • 正規表現ライクなLua Patternsを使う
    • 関数を使う
  • Treesitterとの組み合わせもラクチン

あたりが特徴でしょうか。

個人的にはTreesitter対応を魅力に感じていますが、パーサーがない場合のフォールバック処理を自前で用意する必要があり、まだ導入に至っていません。

なにはともあれ、いくつかLuaPatternsを自作してみましょう。

ちなみに本記事で紹介した内容をもっと推し進めたものが以下にあります。

https://github.com/atusy/dotfiles/blob/d08d88f60d74ed0956a4f1c01b9d05269fa35b55/dot_config/nvim/lua/plugins/textobj.lua

テキストオブジェクトを自作する

i[[ foo ]の両端のスペースを含めた範囲を選択する

mini.aiの標準設定では、vi[だと両端のスペースを含まず、vi]で含みます。

mini.ai導入前にvi[を使っていた人はびっくりしそうですね。

設定は簡単で、setup関数でcustom_textobjectsを以下のように記述します。同様に{[<なども設定すると良いでしょう。

require('mini.ai').setup({
  custom_textobjects = {
    ['['] = { '%b[]', '^().().*().()$' }
  },
})

キーの['['] = ...は読みにくいですが、i[およびa[の定義であることを示します。 Luaではテーブルのキーに複雑な文字列を指定する時に'[' = ...とするのではなく、['['] = ...とするのですね。初見では戸惑うかもしれません。

値の{ '%b[]', '^().().*().()$' } は更に独特ですね。 mini.aiでは、Lua Patternsをテーブルに複数記述することで、左から順にマッチする箇所を絞り込めます。更に最後の要素ではキャプチャ(())を使って、a[i[の両端の位置を決められます。

今回の場合、%b[]で対となる[]を探しています。 %bの重要性は以下テキストで、|がカーソル位置の場合にどの範囲を選択したいか考えてみると良いでしょう。

[ [ | [ foo ] ] ]

a[期待するのは[ [ foo ] ]です。しかし、馴染み深い .* による最長一致などを使うと想定外の範囲を選択します。

  • [.*]: [ [ foo ] ] ].*は最長一致)
  • [.-]: [ [ foo ].-は最短一致)
  • %b[]: [ [ foo ] ]

そして、^().().*().()$は、i[a[の選択範囲を示します。 1番目と4番目の()a[の選択範囲で2番目と3番目の()i]の選択範囲です。

今回の例だと、

  1. %b[][ [ foo ] ] を選択
  2. ^().().*().()$で、1で選択された範囲からa[i[の位置を指定

ということが起きています。

ちなみにa[の両端を示す()は以下のように省略可能です。

require('mini.ai').setup({
  custom_textobjects = {
    ['['] = { '%b[]', '^.().*().$' }
  },
})

a][[ foo ]]のような二重カッコを選択する

Lua言語など、一部の言語では二重カッコ([[]])が特別な意味を持ちます。

v2a[などとして、重なる回数を明示する手もありますが、よく使うならvi]とできると便利ですね。 i[i]が同じである必要はありません。

require('mini.ai').setup({
  custom_textobjects = {
    ['['] = { '%b[]', '^.().*().$' }
    [']'] = { '%b[]', '^.%[().*()%].$' }  -- va] で [[ foo ]] 全体を選択できるようになる
  },
})

正規表現の解読は読者にお任せするとして、2a[a[の違いを考えてみましょう。以下の例で|にカーソルがあるとします。

[[  [ foo | ]  ]]

2a[では、[[の連続性は加味せず[の数だけを見るので、[ [ foo ] ]を選択してしまいます。 a]なら、正しく[[ [ foo ] ]]を選択できます。

aj]「 foo 」のような日本語のカッコを選択する

私はブログなどもNeovimで書いてますが、この時、日本語のカッコをtextobjectで扱えると便利です。

あいにく、custom_textobjectsはキーに使える文字が一つですが、値にはLua Patternsを返却する関数を記述できます。

この性質を利用して、vijと打ったらgetchar()でもう1文字を入力させ、入力文字に合わせて挙動を変えるといいでしょう。

require('mini.ai').setup({
  custom_textobjects = {
    ['j'] = function()
      -- キーを入力させる
      local char = vim.fn.nr2char(vim.fn.getchar())

      -- 入力が`[`だったら、「」を対象とするLua Patternsを返す
      if char == '[' then
        return {'^「().-()」$'}
      end

      -- 他はエラー
      error('j' .. char .. ' is unsupported')
    end
  },
})

i[の例では%b[]を使いましたが、あいにく非ASCII文字(?)は非対応なようで、%b「」は機能しません。幸い、「あ「い」う」などとネストすることはまずないので、最短一致の^「().-()」$を使います。

関数を使うと途端に自由度が上がるので、たとえばi [mini.aiのデフォルトのi[のように空白を除去した範囲を選択するなんてこともできます。

ENJOY!