0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

NeoVim で LSP を使ってファイルをリネームする

Posted at

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 クライアントは以下の手順でサーバーの特定のメソッドの呼び出しや通知を行います。

  1. LSP クライアントがサーバーの workspace/willRenameFiles メソッドを呼び出す
  2. サーバーがファイルの変更箇所を返す (返さないこともある)
  3. クライアントがファイルの変更箇所を適用する
  4. クライアントが実際にファイル名を変更する
  5. クライアントがサーバーに 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 はちゃんと変更箇所を返してくれるので嬉しいですね。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?