🌙

neovimプラグインの作成 lua

2024/12/11に公開

過去に書いたもの

https://zenn.dev/tositada/articles/b9190cce45812f
https://zenn.dev/vim_jp/articles/31e60fbf12712b
https://zenn.dev/tositada/articles/609c75cea1208a

luaを使用してpluginを開発する方法について学びたいと思います。
この記事は前回の記事の続きからになっています。
先に読んでおくと理解しやすいと思います。

対象者

  • vimの基本操作を理解していること,入力方法,検索方法,コマンドモード,visualモード等
  • luaでneovimのpluginを作成したい人
  • luaのpluginを読んでみたい人
  • lazyvimを使い始めた人

動作環境

  • Arch系 Linux (garuda linux)

https://garudalinux.org/

OS詳細情報
$ uname -a
Linux tosi 6.6.9-zen1-1-zen #1 ZEN SMP PREEMPT_DYNAMIC Tue, 02 Jan 2024 02:28:04 +0000 x86_64 GNU/Linux

$ cat /etc/os-release
File: /etc/os-release
NAME="Garuda Linux"
PRETTY_NAME="Garuda Linux"
ID=garuda
ID_LIKE=arch
BUILD_ID=rolling
ANSI_COLOR="38;2;23;147;209"
HOME_URL="https://garudalinux.org/"
DOCUMENTATION_URL="https://wiki.garudalinux.org/"
SUPPORT_URL="https://forum.garudalinux.org/"
BUG_REPORT_URL="https://gitlab.com/groups/garuda-linux/"
PRIVACY_POLICY_URL="https://terms.archlinux.org/docs/privacy-policy/"
LOGO=garudalinux

  • neovim NVIM v0.10.2
neovim詳細情報
$ nvim --version
NVIM v0.10.2
Build type: RelWithDebInfo
LuaJIT 2.1.1702233742
Run "nvim -V1 -v" for more info

プラグインのファイル構造

まずはプラグインのファイル構造から考えたいと思います。

plugin/
フォルダ内のファイルはpluginのロード時に起動時に実行されます。
pluginと同じ名前で保存されています。

lua/
ほとんどの場合プラグインの起動時にすべてを実行することは望ましくありません。
ファイルを開いたとき、コマンドが呼び出されたときなど、何かしらイベントに応じて関数を呼び出す場合このファイル内で整理します。

ディレクトリーを呼び出すには、runtimepathによって検索されrequireで呼び出します。
runtimepathではrequire(foo.bar)と書かれている場合lua/フォルダーの下からlua/foo/bar.luaまたは,lua/foo/bar/init.luaを読み込みます。拡張子.luaは省略してピリオド(.)区切りで表記します。

プラグインのファイル構造の採用例

lazyvimや、terescopeの場合パターン1の構成のディレクトリーを採用しています。
neo-treeや、前回使用したhex.nvimの場合はパターン2の構成のディレクトリーをを採用しています。
参考例として呼び出し部分しか記載していませんが、もちろんファイルは他に存在していて問題ありません。

パターン1
プラグイン名のフォルダーの下にinit.luaを作成して呼び出し

├── lua
│   └── plugin_name
│       └── init.lua

パターン2
luaフォルダーの下にプラグイン名のファイル名を作成して呼び出し

├── lua
│   └── plugin_name.lua

参考一覧

https://github.com/nvim-neo-tree/neo-tree.nvim
https://github.com/RaafatTurki/hex.nvim
https://github.com/LazyVim/LazyVim
https://github.com/nvim-telescope/telescope.nvim/tree/master

プラグインの作成

簡単なpluginとしてhello worldを表示するプラグインを作成します。
1章ではhello worldを固定して表示します。
2章ではメッセージを設定ファイルに保存して表示します。
3章ではコマンド実行時にhello worldを表示します。
4章ではhello worldから変えて bashの機能を呼び出してみます。

以降記事の中でnvim/と記載する場合以下のフォルダー以下を指します。
windows: C:\Users\user\AppData\Local\nvim\
linux: /home/user/.config/nvim

1.hellow worldを固定して表示

作業用のディレクトリーとしてホームディレクトリ内にdev_pluginを作成します。
その下にpluginをmyexampleと名前を付けて保存します。

mkdir ~/dev_plugin/myexample

最初の方にも書きましたがpluginのロード時に起動されるファイルはplugin/になります。
以下のようなファイル構成になるようにフォルダーを作ります。

dev_plugin
└─myexample
    └─plugin
        └─myexample.lua

myexample/plugin/myexample.luaでは単純にhello worldを表示するためのプログラムを書きます。

print("hello world")

lazyvimで自分の環境内にあるpluginを導入するにはdirで呼び出します。
nvim/lua/plugin/myexample.luaを作成します。lazyvimの設定内容は前回の記事に記載しています。

nvim/lua/plugin/myexample

return {
  { dir = "~/dev_plugin/myexample/" },
}

新しくnvimを開いて:messagesをコマンドに入力すると、hellow worldが入力されていることが分かります。

初めてpluginを作成しました。
これでは1つのファイルしか使用できないので、別のファイルを読み込んで実行したいと思います。

2.設定ファイルで文字列を変えてみる。

新しくluaファイルを作成します。
一旦lua/フォルダー以下でpluginが実行できるようにします。
先ほど見たパターン2のファイル構造ですね。

dev_plugin
└─myexample
    ├─lua
    │  └─myexample.lua
    │
    └─plugin
        └─myexample.lua

myexample/lua/myexample.luaprint("Hellow world")を記入して

myexample/lua/myexample.lua

print("Hellow world")

myexample/plugin/myexample.luaにはrequire("myexample")と書いてみよう

myexample/plugin/myexample.lua

require("myexample")

一度nvimを閉じて:messagesをコマンドに入力すると、pluginフォルダーからluaフォルダー以下を呼び出していることが分かります。

:Lazyのコマンドでlazy.nvimを呼び出すと、myexampleがロードされていることに気づきます。


別の場所にファイルを設定できるようにしたので、設定ファイルで文字列を変えてみる。ことに戻ります。
myexample/lua/myexample.luaを編集します。

local M = {}

M.config = {
	message = "hello world",
}

M.setup = function(args)
	M.config = vim.tbl_deep_extend("force", M.config, args or {})
	print(M.config.message)
end

return M

一瞬複雑なコードを見てうぅとなるかもしれませんが説明します。
1行目にモジュール名を宣言して最後に
returnでモジュール返すことでpluginが機能します。

仮にモジュール名をMとしていますがMをすべてHelloに変えても動作します。
実際のプラグインでもMにしている人がいたりtelescopeなどplugin名にしている人もいます。
nvimでpluginを呼び出した場合setup()関数が自動的に呼び出されます。
luaの関数はfunction()で始まりendで終わります。
vim.tbl_deep_extend()でpluginマネージャから引き受けたconfigの項目をM.configに結合して、printで結果を表示します。

lazyvimの設定はこのようになります。
一度nvimを閉じて起動した場合にgood byeが表示されると思います。

nvim/lua/plugin/myexample

return {
  {
    dir = "~/dev_plugin/myexample/",
    config = {
      message = "good bye",
    },
  },
}

3.コマンド作成

起動時に実行するのもいいですが特定の動作をした際に起動したいといったこともあります。
そのような場合には関数からコマンドを作成しておき、特定の動作の時に実行することができます。

myexample/lua/myexample.lua

local M = {}

M.config = {
	message = "hello world",
    first_name = "yamda",
    nickname = "taro",
}

M.hello_name = function()
    print(M.config.message..M.config.first_name)
end

M.hello_nick = function()
    print(M.config.message..M.config.nickname)
end


M.setup = function(args)
	M.config = vim.tbl_deep_extend("force", M.config, args or {})
	print(M.config.message)
    vim.api.nvim_create_user_command('HelloName', M.hello_name, {})
    vim.api.nvim_create_user_command('HelloNick', M.hello_nick, {})
end

return M

M.hello_nick(),M.hello_name()の関数を作成しました。
M.setup()関数内でvim.api.nvim_create_user_command()で関数をvimに登録します。
vim.api.nvim_create_user_command()の使用方法はコマンドモードで:h nvim_create_user_command()を実行すると確認できます。
nvim_create_user_command({name},{command},{opts})と書かれています。
{name}には新しいコマンドをuppercaseでつけろと書いてあります。
{command}にはコマンドを実行した際に呼び出される。luafunctionと書かれています。
{opts}にはコマンドの説明などを記載するようです。

lazyvimの設定は以下のようにしてコマンドを導入できるようにしておきます。
一度nvimを終了して:HelloName,:HelloNickとすると反応が返ってくると思います。

nvim/lua/plugin/myexample

return {
  {
    dir = "~/dev_plugin/myexample/",
    config = {
      message = "good bye ",
      first_name = "yamamoto"
      nickname = "ziro"
    },
    cmd = {
      "HelloName",
      "HelloNick",
     }
  },
}

4.bashのコマンド実行

単純なcommnadとしてログインユーザーを確認するコマンドを実行してみます。
bashでwhoを実行するとログインユーザーが確認できます。
awkを実行するとデータファイルの処理を行えます例として1列目だけを表示します。

$ who 
tosi     tty2         2024-11-26 17:29 (:0)
root     pts/1        2024-11-26 17:29 (:0)
$ who | awk '{print $1}' 
tosi
root

これを使用してログインユーザーに挨拶して見ようと思います。

myexample/lua/myexample.lua

local M = {}

M.config = {
	message = "hello world",
	first_name = "yamda",
	nickname = "taro",
}

M.hello_name = function()
	print(M.config.message .. M.config.first_name)
end

M.hello_nick = function()
	print(M.config.message .. M.config.nickname)
end

M.hello_loginuser = function()
	local job1 = vim.system({ "who" }):wait()
	local job2 = vim.system({ "awk", "{print $1}" }, { stdin = job1.stdout }):wait()
	vim.iter(vim.gsplit(job2.stdout, "\n")):each(function(line)
        if line ~= "" then
            print(line .. " Hello")
        end
	end)
end

M.setup = function(args)
	M.config = vim.tbl_deep_extend("force", M.config, args or {})
	vim.api.nvim_create_user_command("HelloName", M.hello_name, {})
	vim.api.nvim_create_user_command("HelloNick", M.hello_nick, {})
	vim.api.nvim_create_user_command("HelloLoginuser", M.hello_loginuser, {})
end

return M

luaからbashのコマンドを使用するにはvim.system()を使用します。
bash上ではpipe|が使えますがlua上では使用できないのでjob1の実行結果をjob2のstdinに設定して実行します。
vim.iter他の言語でいうところの「配列」や「連想配列」に対して様々な操作をするための関数です。
コマンドの実行結果を改行ごとに分割して、それぞれにHelloと出力します。
内部に何かしらの文字列があるときだけ helloと表示します。

nvim/lua/plugin/myexample

return {
  {
    dir = "~/dev_plugin/myexample/",
    dependencies = {
      "nvim-lua/plenary.nvim",
    },
    config = {
      message = "good bye ",
      first_name = "yamamoto",
      nickname = "ziro",
    },
    cmd = {
      "HelloName",
      "HelloNick",
      "HelloLoginuser",
    },
  },
}

最後に

Neovim プラグインを開発する場合、開始方法を調べることが最も難しい部分に思います。
自分もこのzennの記事を作成するにあたり複数の英語のページ見てきました。
このチュートリアルがお役に立てば幸いです。
インデントめちゃくちゃになってる...
移行できたらGitHub管理で使ってみます。

参考文献

https://m4xshen.dev/posts/develop-a-neovim-plugin-in-lua
https://github.com/ellisonleao/nvim-plugin-template
https://zignar.net/2022/11/06/structuring-neovim-lua-plugins/
https://neovim.io/doc/user/lua.html
https://qiita.com/delphinus/items/2c993527df40c9ebaea7

Discussion