Vim/Neovimプラグイン作成入門

2022/11/15に公開

Vim/Neovimユーザーの方ならまず間違いなく何らかのプラグインを利用していると思いますが、全員がその作り方や仕組みまで理解しているわけではないでしょう。かく言う私も10年近くVimを使ってきましたが、プラグインの中身についてはよくわかっていませんでした。

というわけで、今回は Vim/Neovim向けのプラグインをVimScript/Luaで作成してみました。最小限の構成で、かつある程度実用性のあるものになります。

実際にプラグインを作ろうとしている方はもちろん、使っているプラグインの中身を理解するのにも役立つかと思いますので是非読んでみてください。

フォルダ構成

example-plugin という名前のプラグインのフォルダの構成はこのようになります。フォルダの場所はどこでもOKです。この構成でGithubのリポジトリを作成すれば公開できます。

公開するならREADME.md, LICENSE, doc/example-plugin.txtは作るべきですし、他にもscripts, testsなどのフォルダがありますが今回は省きます。

example-plugin
  ├── plugin
  │  └── example.vim
  ├── autoload
  │  └── example.vim
  └── lua
     └── example
        ├── init.lua
        └── hello.lua

プラグインをローカルのホームフォルダに置いた場合の設定ファイルの記述はこのようになります。お使いのプラグインマネージャに合わせて変更ください。

.config/nvim/init.lua
require("packer").startup(function()
  use '~/example-plugin'
end)

plugin/example.vim

プラグインロード時に読み込まれるファイルです。プログラムのエントリーポイントみたいなものです。Vim ScriptプラグインもLuaプラグインでも共通です。

初期化関数の実行、コマンドの公開、Vim/Neovim本体のバージョンチェック、プラグインが二重に読み込まれるのを防止する処理を行っています。他にも、デフォルトのキーバインドの記述もここに書いておくといいでしょう。

plugin/example-plugin.vim
if exists('g:loaded_example_plugin')
  finish
endif
let g:loaded_example_plugin = 1

call example#IMEStateSaveEnable()

command! -nargs=0 IMEStateSaveEnable  call example#IMEStateSaveEnable()
command! -nargs=0 IMEStateSaveDisable call example#IMEStateSaveDisable()
command! -nargs=0 IMEStateSaveToggle  call example#IMEStateSaveToggle()

if !has('nvim')
  command! -nargs=0 ExampleOpenWin  lua require("example").nvim_open_win()
endif

autoload/example.vim

VimScriptによる関数を登録します。ファイル名#関数名という命名規則に従います。

今回例として作成した機能は、INSERTモードからNORMALモードに移る時にIME (fcitx5) の状態をオフにし、再度INSERTモードに入る時に元の状態に戻すというものです。autocmd,autogroupの機能を利用して実装しています。日本語の文章を書く時に重宝します。

autoload/example.vim
" $ fcitx5-remote -h
" Usage: fcitx5-remote [OPTION]
"         -c              inactivate input method
"         -o              activate input method
"         [no option]     display fcitx state, 0 for close, 1 for inactive, 2 for active
"         -h              display this help and exit

function! example#IMEStateSaveEnable() abort
  if executable('fcitx5-remote')
    let g:fcitx_state = 1
    augroup IMEStateSave
      autocmd!
      autocmd InsertLeave * let g:fcitx_state = str2nr(system('fcitx5-remote'))
      autocmd InsertLeave * call system('fcitx5-remote -c')
      autocmd InsertEnter * call system(g:fcitx_state == 1 ? 'fcitx5-remote -c': 'fcitx5-remote -o')
    augroup END
  endif
endfunction

function! example#IMEStateSaveDisable() abort
  augroup IMEStateSave
    autocmd!
  augroup END
endfunction

function! example#IMEStateSaveToggle() abort
  if !exists('#IMEStateSave#InsertEnter')
    call example#IMEStateSaveEnable()
  else
    call example#IMEStateSaveDisable()
  endif
endfunction

lua/example/*.lua

Luaによる関数を実装します。Luaモジュール本体と、必要に応じてサブモジュールを実装します。Mというテーブルにメソッドを追加してモジュールとして公開するのが一般的です。

使用できるAPIやLuaの文法は下記を参照してください。実装の状況にもよりますが、VimScriptでできることはだいたいLuaでもできます。


今回実装した機能は、エディタ右下に80x30のフロートウィンドウを作成するというものです。メモする時に使えるんじゃないでしょうか。より実用的なものを目指すなら本当はウィンドウのサイズによっていい感じに調整できるようにしたいですが、vim.api.nvim_open_win関数の紹介という意味合いで簡単なものにしています。

また、バッファ番号を保存するためにクラスを活用していますが、この使い方で合ってるのかはぶっちゃけ自信ないです。

lua/example/init.lua
local hello = require("example.hello")

win = hello.Window:new()

local M = {}

function M.nvim_open_win()
  h = vim.opt.lines['_value'] - vim.o.cmdheight
  w = vim.o.columns

  vim.api.nvim_open_win(win.bufnr, true, {
    relative='win',
    anchor="NW",
    width=80,    -- ウィンドウの幅
    height=30,   -- ウィンドウの高さ
    col=w-80-5,  -- 右からの距離
    row=h-30-3,  -- 上からの距離
    focusable=true,
  })
end

return M
lua/example/hello.lua
local Window = {}

function Window:new()
  local w = {}

  w.bufnr = vim.api.nvim_create_buf(false, true)

  return w
end

M = {}

M.Window = Window

return M


また、*.lua*ファイルを開いた状態で:luafile %とするとVim内でLuaを実行することができます。関数の機能などを確認する時に便利です。プラグインの開発中で覚えておくといいと思います。

参考

NeovimでLuaプラグインを作成するやり方について丁寧に書いてありますので、合わせて読むとわかりやすいと思います。

https://www.linode.com/docs/guides/writing-a-neovim-plugin-with-lua/

Discussion