neovimでGoのテストファイル移動をチョット楽にする方法
あいさつ
こんにちは。普段は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
内で読み込み処理を行います。
require("ninomae.scripts")
require("ninomae.scripts.go_test_open")
このように設定を記載すると、neovimが起動した際に、go_test_open.lua
も自動で読み込まれます。
実際のモジュール
ここからは、実際にモジュールを作成していきます。
現在カーソルがあるファイルのバッファの番号を取得する
まずは、現在カーソルがあるバッファの番号を取得します。このバッファ番号を識別子として、ファイルに対する操作を行います。
local bufnr = vim.api.nvim_get_current_buf()
テストファイルの名前を求める(src->test)
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)
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
ファイルを開く
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
キーマップを設定する
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