🤖

nvim-insx というプラグインの紹介

2024/03/24に公開

はじめに

この記事では、自作プラグインである nvim-insx について紹介します。
自分の英語力が低いので GitHub の README.md を見ても何ができるのかよくわからんと思ったので、日本語で記事を書くことにしました。

nvim-insx とはなんのプラグインなのか?

端的に言ってしまえば、下記の 2 点を提供します。

  • キーマッピングを便利に定義するための コア API
  • 事前定義の便利設定集である レシピ API
    • 例えば、いわゆる auto-pairs 系の機能がビルトインで提供されます

nvim-insx をインスタントに利用するだけであれば、コア API に習熟する必要はなく、ヘルプに記載されている レシピ API を自分好みに使うだけでよいでしょう。

この記事では実際のユースケースを参考にしながら コア API の使い方を解説します。

コア API

簡単な例題として マルチバイト文字列中の ASCII 文字列にスペースを付与する 機能を作ってみましょう。下記のような機能です。

before input after
開発者コミュニティ|Zennで記事を書こう <Space> 開発者コミュニティ |Zenn で記事を書こう

※ 「ASCII 文字の左右にスペースを入れる」 という、よくある操作を自動化したいということです。

まず、この処理は <Space> を打鍵した際に発動してほしいので、そのように nvim-insx に指示をします。

local insx = require('insx')
insx.add('<Space>', {
  ... あとで書く ...
})

次に、この処理は「どういった条件で発動してほしいか?」ということを指示する必要があります。
nvim-insx では「どういった条件で発動すべきか?」を enabled キーに指定できます。

local insx = require('insx')
insx.add('<Space>', {
  enabled = function(ctx)
    return ... あとで書く ...
  end
})

今回の例でいうと カーソルの左側がマルチバイト文字で、カーソルの右側がアスキー文字の連続であり、スペースが入ってない 場合に発動してほしい気がします。nvim-insx はカーソル周辺のテキストに関しては便利機能を多数提供しているのでそれを使って判定します。

local insx = require('insx')
insx.add('<Space>', {
  enabled = function(ctx)
    -- カーソル左側がマルチバイト文字である
    local before_non_ascii = ctx.match([=[[^\x00-\x7F]\%#]=])
    -- カーソル右側が ASCII 文字列で、スペースなしでマルチバイトが続いている
    local after_ascii_without_space = ctx.match([=[\%#[\x00-\x7F]\+[^\x00-\x7F]]=])
    return before_non_ascii and after_ascii_without_space
  end
})

\%# は vim 正規表現における カーソル位置 を表すエスケープシーケンスです。

次に どういうテキスト編集を行うか? を定義しましょう。
nvim-insx では、実際の編集操作は action キーに指定します。

local insx = require('insx')
insx.add('<Space>', {
  enabled = function(ctx)
    -- カーソル左側がマルチバイト文字である
    local before_non_ascii = ctx.match([=[[^\x00-\x7F]\%#]=])
    -- カーソル右側が ASCII 文字列で、スペースなしでマルチバイトが続いている
    local after_ascii_without_space = ctx.match([=[\%#[\x00-\x7F]\+[^\x00-\x7F]]=])
    return before_non_ascii and after_ascii_without_space
  end,
  action = function(ctx)
    ... あとで書く ...
  end
})

さて、今回の例では action には何を書くべきでしょうか?
先に動作する実装を提示して、それを補足するという方向で説明していきます。

local insx = require('insx')
insx.add('<Space>', {
  enabled = function(ctx)
    -- カーソル左側がマルチバイト文字である
    local before_non_ascii = ctx.match([=[[^\x00-\x7F]\%#]=])
    -- カーソル右側が ASCII 文字列で、スペースなしでマルチバイトが続いている
    local after_ascii_without_space = ctx.match([=[\%#[\x00-\x7F]\+[^\x00-\x7F]]=])
    return before_non_ascii and after_ascii_without_space
  end,
  action = function(ctx)
    ctx.send('<Space>') -- ...①
    local row, col = ctx.row(), ctx.col() -- ...②
    ctx.move(unpack(ctx.search([=[\%#[\x00-\x7F]\+\zs]=]))) -- ...③
    ctx.send('<Space>') -- ...④
    ctx.move(row, col) -- ...⑤
  end
})

何やら呪いじみていますが、一つずつ見ていきます。

最初のテキストの状態は下記のような状態のはずです。

開発者コミュニティ|Zennで記事を書こう

① は、単純にスペースを挿入しているだけなので、下記のようになります。

開発者コミュニティ |Zennで記事を書こう

② は、現在のカーソル位置を変数 row/col に保存しています。
最終的に ⑤ の段階でカーソル位置を上記の位置に戻すためです。

③ では ASCII 文字列の末尾 にカーソルを移動させています。

開発者コミュニティ Zenn|で記事を書こう

④ では、またまた単純にスペースを挿入しています。

開発者コミュニティ Zenn |で記事を書こう

⑤ では、 ② の段階の位置にカーソルを戻す という処理が書かれています。よって、

開発者コミュニティ |Zenn で記事を書こう

となります。

このように nvim-insx における action人間が手入力でテキストを編集する際のシーケンス を一つひとつ実装するようなメンタルモデルで記述することができます。

終わりに

記事を書く体力があまりないのでこの辺にしておきます。

不明点があれば vim-jp Slack で質問をしていただければ答えることができます。
また、バグレポートは大歓迎です。

Discussion