😺

neovimでGoのテストファイル移動をチョット楽にする方法

2024/03/01に公開

あいさつ

こんにちは。普段はGoを使ってバックエンドの開発をしているninomaeです。
neovimでファイル移動をするには、ファイラやファジーファインダーなどを使ったりいろんな選択肢がありますが、わざわざテストファイルを開くために何文字もタイピングするのはめんどくさいですよね。
そこで今回は、luaを使ってGoのソースファイルとテストファイルの移動をチョット楽にする、neovim用の自作モジュールを開発します。

設定ファイルの置き場所について

neovimの設定ファイルとして検索されるディレクトリは、$XDG_CONFIG_HOME/nvimになります。macの場合は~/.config/nvimになります。ここにinit.luaを配置し、サードパーティのパッケージなどをインポートすることで、neovimの動作をカスタマイズできます。

luaのモジュールの置き場所は~/.config/nvim/luaになります。このluaディレクトリの中に、ご自身のユーザー名などのディレクトリを作り、その中にluaファイルを配置していきます。
今回私のユーザー名はninomae、カスタムスクリプトを配置するディレクトリの名前はscriptsとして以降の議論を進めていきます。

tree ~/.config/nvim

init.lua
└── lua
    └── ninomae
        └── scripts
            ├── go_test_open.lua
            └── init.lua

各ディレクトリのinit.luaが最初に読み込まれるので、必要なモジュールはこのinit.lua内で読み込み処理を行います。

~/.config/nvim/init.lua
require("ninomae.scripts")
~/.config/nvim/lua/ninomae/scripts/init.lua
require("ninomae.scripts.go_test_open")

このように設定を記載すると、neovimが起動した際に、go_test_open.luaも自動で読み込まれます。

実際のモジュール

ここからは、実際にモジュールを作成していきます。

現在カーソルがあるファイルのバッファの番号を取得する

まずは、現在カーソルがあるバッファの番号を取得します。このバッファ番号を識別子として、ファイルに対する操作を行います。

go_test_open.lua
local bufnr = vim.api.nvim_get_current_buf()

テストファイルの名前を求める(src->test)

go_test_open.lua
local get_test_file_name = function(bufnr)
	local file_name = vim.api.nvim_buf_get_name(bufnr)
	local file_type = vim.api.nvim_buf_get_option(bufnr, "filetype")

	if file_type == "go" then
		local is_test_file = file_name:match("_test.go$") ~= nil
		if is_test_file then
			return file_name
		end
		local full_path_without_extension = vim.fn.fnamemodify(file_name, ":r")
		return full_path_without_extension .. "_test.go"
	end
end

fnamemodify({fname}, {mods})関数は、ファイル名・ファイルパスの情報を取得するためのvimの組み込み関数です。{fname}の部分には操作対象のファイル名を、{mods}の部分には修飾子を渡すことで情報を取得できます。

{mods}の有効な引数は、:help fnamemodify()で一覧することができます。今回使用している:rの定義は、

Root of the file name (the last extension removed). When there is only an extension (file name that starts with '.', e.g., ".nvimrc"), it is not removed. Can be repeated to remove several extensions (last one first).

拡張子を取り除いた、ファイルのフルパスを取得できます。

使用例
:echo fnamemodify("main.c", ":p:h")

/home/user/vim/vim/src

ソースファイルからテストファイルの名前を求める(test->src)

go_test_open.lua
local get_source_file_name = function(bufnr)
	local file_name = vim.api.nvim_buf_get_name(bufnr)
	local file_type = vim.api.nvim_buf_get_option(bufnr, "filetype")

	if file_type == "go" then
		local is_test_file = file_name:match("_test.go$") ~= nil
		if not is_test_file then
			return file_name
		end
		local full_path_without_extension = vim.fn.fnamemodify(file_name, ":r")
		local fullpath_without_test = full_path_without_extension:gsub("_test$", "")
		return fullpath_without_test .. ".go"
	end
end

ファイルを開く

go_test_open.lua
local open_file_with_split = function(file_name, split_direction)
	local commands = {
		h = ":split",
		horizontal = ":split",
		v = ":vsplit",
		vertical = ":vsplit",
		default = ":edit",
	}

	local command = commands[split_direction] or commands.default
	vim.cmd(command .. file_name)
end

local open_file = function(type, split_direction)
	-- get current buffer
	local bufnr = vim.api.nvim_get_current_buf()
	local file_name_func = type == "test" and get_test_file_name or get_source_file_name
	local file_name = file_name_func(bufnr)

	if file_name and vim.fn.filereadable(file_name) == 1 then
		print("opening: " .. file_name)
	else
		print("creating: " .. file_name)
	end

	open_file_with_split(file_name, split_direction)
end

キーマップを設定する

go_test_open.lua
vim.keymap.set("n", "<leader>tt", function()
	open_file("test", "")
end, { noremap = true, silent = true, desc = "open test file" })
vim.keymap.set("n", "<leader>th", function()
	open_file("test", "horizontal")
end, { noremap = true, silent = true, desc = "open test file horizontally" })
vim.keymap.set("n", "<leader>tv", function()
	open_file("test", "vertical")
end, { noremap = true, silent = true, desc = "open test file vertically" })

vim.keymap.set("n", "<leader>Tt", function()
	open_file("source", "")
end, { noremap = true, silent = true, desc = "open source file" })
vim.keymap.set("n", "<leader>Th", function()
	open_file("source", "horizontal")
end, { noremap = true, silent = true, desc = "open source file horizontally" })
vim.keymap.set("n", "<leader>Tv", function()
	open_file("source", "vertical")
end, { noremap = true, silent = true, desc = "open source file vertically" })

私の場合はleaderキーを<Space>に割り当てているので、スペースキーに続けて2回tを入力すると同じウィンドウにテストファイルが表示されます

vim.keymap.set({mode}, {lhs}, {rhs}, {opts})関数は、vimのキーマップ設定を行う関数です。{mode}の部分には、キーマップを設定したいvimのモード、{lhs}にはマッピングしたいキー入力、{rhs}には実行したいluaの関数を設定します。
:help vim.keymap.set()で詳細を確認できます。

設定例
 -- Map to a Lua function:
 vim.keymap.set('n', 'lhs', function() print("real lua function") end)
 -- Map to multiple modes:
 vim.keymap.set({'n', 'v'}, '<leader>lr', vim.lsp.buf.references, { buffer=true })
 -- Buffer-local mapping:
 vim.keymap.set('n', '<leader>w', "<cmd>w<cr>", { silent = true, buffer = 5 })
 -- Expr mapping:
 vim.keymap.set('i', '<Tab>', function()
   return vim.fn.pumvisible() == 1 and "<C-n>" or "<Tab>"
 end, { expr = true })
 -- <Plug> mapping:
 vim.keymap.set('n', '[%', '<Plug>(MatchitNormalMultiBackward)')

おわりに

neovimはluaでちょっとしたスクリプトを書くだけで、自分好みに動作をカスタマイズすることができるエディターです。
今回のようにluaの関数を書いてもよいですし、入門としてよく使うvimコマンドをkeymapとして設定するだけでもチョット快適になるはずです。
みなさんもご自分のワークフローに合わせて、カスタマイズしてみてはいかがでしょうか。

Discussion