この記事は何?
大学にて、「大規模ソフトウェアを手探る」というお題で行われた実験で得た知見を後世に伝える記事の子記事である。
CopilotChat.nvimを拡張したプラグインについて記す。
親記事
目的
コードをリファクタリングするという特定の機能を自動化する目的のプラグインを作成した。
リファクタリング
外部から見たプログラムの動作を保ったまま(無駄な処理を省いたりして可読性・保守性を高める)ソースコードを改善すること。
ベースとなるプラグイン
CopilotC-Nvim/CopilotChat.nvim
VSCodeのように、Neovim上でCopilotとのChatが可能なプラグイン。
主な基本機能は以下の通り。
| コマンド | 説明 |
|---|---|
:CopilotChat <input>? |
チャットウィンドウを開く。オプションで <input> に質問や指示を直接入力して開始することも可能。 |
:CopilotChatOpen |
チャットウィンドウを開く。 |
:CopilotChatClose |
チャットウィンドウを閉じる。 |
:CopilotChatToggle |
チャットウィンドウの表示/非表示を切り替える。 |
実装した機能
| コマンド | 説明 |
|---|---|
:Refactorfile |
CopilotのChatを開き、選択しているファイルをリファクタリングしたものを出力する。 |
実装
~/.config/nvim/lua/plugins/copilotchat.luaは以下の通り。
return {
"CopilotC-Nvim/CopilotChat.nvim",
dependencies = {
"github/copilot.vim",
"nvim-lua/plenary.nvim",
},
config = function()
-- CopilotChatの初期設定
require("CopilotChat").setup({
mappings = {
diff = {
apply_diff = "ga", -- AIの提案(Diff)を適用するキーバインド
},
},
debug = false,
})
-- 現在のバッファをリファクタリングする関数定義
local function refactor_current_file()
local chat = require("CopilotChat")
local buf_id = vim.api.nvim_get_current_buf()
-- ファイルタイプを取得 (未設定の場合は 'code' とする)
local filetype = vim.bo.filetype
if not filetype or filetype == "" then filetype = "code" end
-- バッファの全行を取得して文字列に結合
local original_lines = vim.api.nvim_buf_get_lines(buf_id, 0, -1, false)
local original_text = table.concat(original_lines, "\n")
-- 空ファイルの場合は処理を中断
if original_text == "" then return end
-- プロンプト作成:
-- 言語を指定し、「コードブロックのみ」を返すよう指示してチャットのノイズを減らす
local prompt = string.format(
"Please refactor this entire %s code and respond ONLY with the refactored code inside a single code block.\n\n```%s\n%s\n```",
filetype, filetype, original_text
)
-- Copilotにリクエストを送信
chat.ask(prompt, {})
end
-- ユーザーコマンド作成: :RefactorFile で上記関数を実行
vim.api.nvim_create_user_command('RefactorFile', refactor_current_file, {})
end,
}
リファクタリング用のカスタムプロンプト
"Please refactor this entire %s code and respond ONLY with the refactored code inside a single code block.\n\n```%s\n%s\n```"
(このコード全体をリファクタリングし、リファクタリング後のコードを単一のコードブロック内にのみ記載して返信してください。)
このプロンプトを毎回自動で送っていることになる。
ポイント
-
ノイズレスなプロンプト設計
-
respond ONLY with...と指示することで、AI特有の冗長な解説文を排除し、コードブロックのみを出力させている。
-
-
コンテキストの自動注入
-
vim.apiを使用して「現在のファイル全体」と「言語タイプ(filetype)」を自動取得する。これにより、手動コピペの手間を省き、AIが文脈を正確に理解できる。
-
-
修正適用の効率化
- 出力が純粋なコードのみであるため、設定した
gaキー(Diff機能)を使用した際、元のコードへの反映(パッチ適用)がスムーズに行える。
- 出力が純粋なコードのみであるため、設定した
デバッグ
:RefactorFile を実装する過程で、特に解決が困難だった2つの問題について詳細に解説する。
1. エラー①: CopilotChat.nvim が見つからない
現象
Neovim起動直後に :RefactorFile コマンドを実行すると、以下のエラーが発生して処理が停止した。
module 'CopilotChat' not found
原因: Lazy.nvim の「遅延読み込み (Lazy Loading)」
一般的なNeovimの構成では、プラグインマネージャー lazy.nvim を使用して起動速度を最適化する。
- 通常の読み込み: Neovim起動時にすべてのプラグインをメモリにロードする。機能はすぐ使えるが、起動が遅くなる。
- 遅延読み込み: Neovim起動時にはプラグインをロードしない。「特定のコマンドを叩いたとき」や「特定のファイルを開いたとき」など、必要になった瞬間に初めてロードする。
今回のケースでは、init.lua で :RefactorFile コマンドを定義する際、スクリプト内で require("CopilotChat") を実行していた。しかし、この時点ではまだ誰も CopilotChat 機能を使っていなかったため、lazy.nvim はプラグイン本体をロードしておらず、Luaがモジュールを見つけられずにエラーとなった。
解決策: 設定の統合と実行タイミングの制御
カスタムコマンドの定義場所を init.lua から plugins/copilotchat.lua の config 関数内に移動した。
return {
"CopilotC-Nvim/CopilotChat.nvim",
-- ...
config = function()
-- この関数は、CopilotChat.nvim がロードされた「直後」に実行されることが保証される
-- ここで require("CopilotChat") を呼んでも安全
local chat = require("CopilotChat")
-- コマンド定義もここで行う
vim.api.nvim_create_user_command('RefactorFile', ...)
end,
}
これにより、「プラグインがロードされる」→「config関数が走る」→「コマンドが定義される」という正しい順序が確立され、依存関係のエラーが解消した。
2. エラー②: コンテキスト送信の失敗
現象
:RefactorFile を実行し、AIに「リファクタリングして」と指示を送ることはできたが、AIから以下の返答が返ってきた。
"Please provide the C code you want refactored." (リファクタリングしたいCコードを提供してください)
原因: context API の仕様誤認と挙動の不確実性
CopilotChat.nvim には、チャットに追加情報を渡すための context オプションがあり、初めは以下のように実装していた。
chat.ask("リファクタリングして", {
context = {
buffers = { current_buf_id } -- バッファIDを渡せば中身も送ってくれる?
}
})
しかしこのAPIは期待通りに動作しなかった。理由は以下が挙げられる。
-
仕様の複雑さ:
buffersオプションが意図した通りにコード全文をプロンプトに埋め込んでくれるとは限らない(またはバージョンによって挙動が異なる)可能性があった。 - 可視性の欠如: 内部でどのようなプロンプトが生成され、AIに何が送信されているかがブラックボックス化しており、デバッグが困難であった。
解決策: プロンプトへの直接埋め込み
不確実な context API に頼るのをやめ、自前でコード全文を読み取ってプロンプト文字列の一部として結合する方法に転換した。
変更後のロジック:
- Neovim API (
nvim_buf_get_lines) で現在のファイルの全行を取得。 - それを文字列結合し、Markdownのコードブロック形式にする。
- 指示文と結合して、一つの巨大なプロンプトを作成する。
local original_text = ... -- コード全文を取得
local prompt = string.format(
"以下のコードをリファクタリングしてください。\n\n```c\n%s\n```",
original_text
)
-- context オプションは使わず、プロンプトのみで送信
chat.ask(prompt, {})
この「直接埋め込み」により、AIは指示と対象コードを確実に同時に受け取ることができるようになり、即座にリファクタリング結果を返してくれるようになった。

