NeoVim でコードを書いていて、ファイル名を変えたくなるときがあります。ファイル名を変えたときに同時にそれを使用しているインポートなどを変更してほしいですが、そもそも NeoVim にファイラはついていません。VSCode や JetBrains 製品のようにファイル名を変えつつインポートも変更したいので、この記事では開いているファイルをリネームするユーザーコマンドを作成していきます。
環境: NeoVim v0.11.4
参考:
コピペ用完成スクリプト
local function create_rename_params(old_filename, new_filename)
local old_uri = vim.uri_from_fname(old_filename)
local new_uri = vim.uri_from_fname(new_filename)
return {
files = {
{
oldUri = old_uri,
newUri = new_uri,
}
},
}
end
---@param client vim.lsp.Client
---@param edit any
---@param old_filename string
---@param new_filename string
local function post_request_reame(client, edit, old_filename, new_filename)
local rename_params = create_rename_params(old_filename, new_filename)
if edit then
vim.lsp.util.apply_workspace_edit(edit, "utf-8")
end
vim.fn.rename(old_filename, new_filename)
vim.api.nvim_command("edit " .. new_filename)
local success = client:notify("workspace/didRenameFiles", rename_params)
if not success then
vim.notify("Failed to notify language server of file rename", vim.log.levels.ERROR)
end
end
---@param client vim.lsp.Client
---@param old_filename string
---@param new_filename string
local function request_rename(client, old_filename, new_filename)
local rename_params = create_rename_params(old_filename, new_filename)
vim.notify("Using " .. client.name, vim.log.levels.INFO)
client:request("workspace/willRenameFiles", rename_params, function(err, result)
if err then
vim.notify("Error from language server: " .. err.message, vim.log.levels.ERROR)
return
end
post_request_reame(client, result, old_filename, new_filename)
end)
end
vim.api.nvim_create_user_command("RenameFile", function(opts)
local old_filename = vim.api.nvim_buf_get_name(0)
local new_filename = vim.fs.abspath(opts.fargs[1])
local clients = vim.lsp.get_clients({ bufnr = 0, method = "workspace/willRenameFiles" })
if #clients == 0 then
vim.notify("No LSP clients support workspace/willRenameFiles", vim.log.levels.ERROR)
return
elseif #clients == 1 then
request_rename(clients[1], old_filename, new_filename)
return
end
vim.ui.select(clients, {
prompt = "Select LSP client",
format_item = function(item)
return item.name
end,
}, function(client)
if client then
request_rename(client, old_filename, new_filename)
end
end)
end, {
nargs = 1,
complete = "file",
desc = "Rename current file with integrated LSP support",
})
リネーム時に LSP 上で何が起こっているか
ファイルをリネームする際、LSP クライアントは以下の手順でサーバーの特定のメソッドの呼び出しや通知を行います。
- LSP クライアントがサーバーの
workspace/willRenameFiles
メソッドを呼び出す - サーバーがファイルの変更箇所を返す (返さないこともある)
- クライアントがファイルの変更箇所を適用する
- クライアントが実際にファイル名を変更する
- クライアントがサーバーに
workspace/didRenameFiles
を通知する
実装
コマンドの実装
まずは外側のコマンドを実装します。:RenameFile <new filename>
で現在開いているバッファのファイルを新しい名前に変えるコマンドを作成します。
vim.api.nvim_create_user_command("RenameFile", function(opts)
local old_filename = vim.api.nvim_buf_get_name(0)
local new_filename = vim.fs.abspath(opts.fargs[1])
-- 後続の処理
end, {
nargs = 1,
complete = "file",
desc = "Rename current file with integrated LSP support",
})
LSP の選択
バッファにアタッチされている LSP クライアントのうち特定のメソッドをサポートしているものは vim.lsp.get_clients
関数で取得することができます。サポートしているクライアントのリストが帰ってくるので、複数あったら vim.ui.select
により選択ダイアログを表示させます。
local clients = vim.lsp.get_clients({ bufnr = 0, method = "workspace/willRenameFiles" })
if #clients == 0 then
vim.notify("No LSP clients support workspace/willRenameFiles", vim.log.levels.ERROR)
return
elseif #clients == 1 then
-- リネーム処理
return
end
vim.ui.select(clients, {
prompt = "Select LSP client",
format_item = function(item)
return item.name
end,
}, function(client)
if client then
-- リネーム処理
end
end)
workspace/willRenameFiles の呼び出し
選択したクライアントの workspace/willRenameFiles
を呼び出します。NeoVim の API では提供されていないため直接リクエストを飛ばします。リクエストが成功すればファイルの変更箇所 (もしくは nil
) が返ってくるはずです。
local rename_params = create_rename_params(old_filename, new_filename)
vim.notify("Using " .. client.name, vim.log.levels.INFO)
client:request("workspace/willRenameFiles", rename_params, function(err, result)
if err then
vim.notify("Error from language server: " .. err.message, vim.log.levels.ERROR)
return
end
-- 後続の処理
end)
local function create_rename_params(old_filename, new_filename)
local old_uri = vim.uri_from_fname(old_filename)
local new_uri = vim.uri_from_fname(new_filename)
return {
files = {
{
oldUri = old_uri,
newUri = new_uri,
}
},
}
end
documentChanges の適用と実際のリネーム
vim.lsp.util.apply_workspace_edit
を使うとファイルの変更箇所を適用することができます。その後実際のリネーム処理を行います。今回は開いているファイルをリネームするためバッファも新しいファイルを開き直すことにします。
local rename_params = create_rename_params(old_filename, new_filename)
-- 変更箇所の適用
if result then
vim.lsp.util.apply_workspace_edit(result, "utf-8")
end
-- 実際にリネーム
vim.fn.rename(old_filename, new_filename)
-- バッファを開き直す
vim.api.nvim_command("edit " .. new_filename)
後処理
サーバーに workspace/didRenameFiles
を通知します。
local success = client:notify("workspace/didRenameFiles", rename_params)
if not success then
vim.notify("Failed to notify language server of file rename", vim.log.levels.ERROR)
end
まとめ
以上のステップを踏むと LSP から変更箇所を受け取りながらファイル名を変更することができます。なお、スクリプト全体は記事の冒頭の折りたたみに掲載しています。
余談ですが執筆時点で ts_ls や denols、pyright など対応していない LSP が結構あって悲しいなあと思っています。rust-analyser はちゃんと変更箇所を返してくれるので嬉しいですね。