LLMの技術的成果が日に日に突きつけられて喉元まで迫っているのを感じる今日この頃、さすがにろくに使いこなせないのでは困るため、ついにGitHub Copilotを試すことにした。LLMを応用した検索エンジン(perplexity.ai)は以前から使っていたが、開発環境と密に統合されるとどんな具合になるのかはやはり気になるところである。
僕の開発環境はNeovimなのでそれに合わせてCopilotを設定していく。GitHub Copilotは最低月額10ドルからのサブスクリプションサービスなのだが、契約完了時に送られてきたクイックガイドにNeovim向けのものがあったのには少々驚かせられた。大抵Vim向けの設定は各々が手弁当で勝手に生やしている印象が強かったからだ。なんならNeovimのプラグインも公式で用意されている。
だが、本稿ではあえてこれを用いず有志がLuaで書き直したcopilot.luaの方を使う。公式のものと比べて処理の効率化が図られている。プラグインマネージャにはlazy.nvimを用いる。まずは下記の通りにプラグインを導入して起動時に読み込まれるように設定する。
1--lazy
2{ "zbirenbaum/copilot.lua", cmd = "Copilot" },
3
4--copilot
5require("copilot").setup({
6 suggestion = { enabled = false },
7 panel = { enabled = false },
8})
上記の設定でsuggestion
がfalse
な理由は、標準とは異なる形でコード支援を受けるためだ。true
にすると任意のキーでエディタ上にコードの予測が現れる。また、auto_trigger
を項目に追加してtrue
に設定すると自動的に表示されるようになる。しかし、本稿では補完プラグインのnvim-cmpと連携を行うのでこれらは無効化しておく必要がある。
次に、一旦Copilotの認証を済ませる。プラグインが読み込まれた後に:Copilot auth
を実行すると、時限制のトークンコードと共にGitHubの認証ページに案内される。自前で作成したファイルにsecretを格納する形式(誤って公開リポジトリに上げて後で痛い目を見るやつ)かと思っていたので、さすが今時は違うなと素直に感心した次第だ。
続いてnvim-cmpと連携を行う。標準設定ではエディタ上に薄くハイライトされるが、この設定では補完候補の一つとして表示される。個人的には、時として納得のいかないサジェストが前面にぶわっと出るよりは補完候補の一つに収まってくれる方が好ましいと感じる。nvim-cmp本体の詳細な設定は割愛させて頂く。
1--lazy
2{ "zbirenbaum/copilot-cmp", config = true, event = "InsertEnter" },
3
4--nvim-cmp
5local cmp = require("cmp")
6
7--他の設定は省略
8
9sources = cmp.config.sources({
10 { name = "nvim_lsp", max_item_count = 15, keyword_length = 2 },
11 { name = "vsnip", max_item_count = 15, keyword_length = 2 },
12 { name = "copilot", max_item_count = 15, keyword_length = 2 }, --copilotを補完ソースに追加する
13 { name = "nvim_lsp_signature_help" },
14 { name = "buffer", max_item_count = 15, keyword_length = 2 },
15}),
16})
17
18local capabilities = require("cmp_nvim_lsp").default_capabilities()
max_item_count
は補完候補の最大量でkeyword_length
は補完が発動する最小のキーワード数を意味する。デフォルト値で差し支えなければ削っても構わない。上記では他にも補完ソースが記述されているが、当然ながらどれも個別に設定していないと動かない点に留意されたし。
1local lspkind = require("lspkind")
2
3formatting = {
4 format = lspkind.cmp_format({
5 mode = "symbol",
6 maxwidth = 50,
7 ellipsis_char = "...",
8 symbol_map = { Copilot = "" },
9 }),
10},
lspkind.nvimを利用している場合は補完候補のシンボルに任意の絵文字を配置できる。きれいな絵文字が並んでいるとモチベが上がるのでぜひ設定したい。なお、次候補の選択をTabキーで行っている人は以下の特殊な設定を追記しないとバグるらしい。
1local has_words_before = function()
2 if vim.api.nvim_buf_get_option(0, "buftype") == "prompt" then
3 return false
4 end
5 local line, col = unpack(vim.api.nvim_win_get_cursor(0))
6 return col ~= 0 and vim.api.nvim_buf_get_text(0, line - 1, 0, line - 1, col, {})[1]:match("^%s*$") == nil
7end
8cmp.setup({
9 mapping = {
10 ["<Tab>"] = vim.schedule_wrap(function(fallback)
11 if cmp.visible() and has_words_before() then
12 cmp.select_next_item({ behavior = cmp.SelectBehavior.Select })
13 else
14 fallback()
15 end
16 end),
17 },
18})
以上でGitHub Copilotの提案が補完候補に表示されるようになる。不要な時は次の候補に飛ばせばいいだけなので馴染みやすいと思われる。たとえCopilotの重要性が今後増してくるとしても、既存のLSPによる補完が依然有効なのは変わりない。
さて、これでCopilotの設定は終わりかと思いきや、実はそうではない。LLM最大の機能と言うべきは対話による改善なので、チャットができないことには魅力半減だ。しかし公式プラグインにも今回紹介した非公式の方にもチャット機能は備わっていない。そこで、CopiotChat.nvimというさらに別のプラグインを導入する。
CopilotChat.nvimは名前通りCopilotとの対話機能を提供するプラグインである。数多くの連携プラグインが用意されており設定内容も多岐に渡るが、本稿では自由記述方式と定型文で質問を行う方法について記す。後者にはtelescope.nvimとの連携機能を用いている。
1--lazy
2{ "CopilotC-Nvim/CopilotChat.nvim", build = "make tiktoken" },
3
4--CopilotChat
5local select = require("CopilotChat.select")
6
7require("CopilotChat").setup({
8 debug = true,
9
10 window = {
11 layout = "float",
12 relative = "editor",
13 },
14 prompts = {
15 Explain = {
16 prompt = "/COPILOT_EXPLAIN 選択されたコードの説明を段落をつけて書いてください。",
17 },
18 Review = {
19 prompt = "/COPILOT_REVIEW 選択されたコードをレビューしてください。",
20 callback = function(response, source) end,
21 },
22 Fix = {
23 prompt = "/COPILOT_FIX このコードには問題があります。バグを修正したコードに書き直してください。",
24 },
25 Optimize = {
26 prompt = "/COPILOT_REFACTOR 選択されたコードを最適化してパフォーマンスと可読性を向上させてください。",
27 },
28 Docs = {
29 prompt = "/COPILOT_DOCS 選択されたコードに対してドキュメンテーションコメントを追加してください。",
30 },
31 Tests = {
32 prompt = "/COPILOT_TESTS 選択されたコードの詳細な単体テスト関数を書いてください。",
33 },
34 FixDiagnostic = {
35 prompt = "ファイル内の次のような診断上の問題を解決してください:",
36 selection = select.diagnostics,
37 },
38 },
39})
40
41function CopilotChatBuffer()
42 local input = vim.fn.input("Quick Chat: ")
43 if input ~= "" then
44 require("CopilotChat").ask(input, { selection = require("CopilotChat.select").buffer })
45 end
46end
47
48vim.api.nvim_set_keymap("n", "<leader>9", "<cmd>lua CopilotChatBuffer()<cr>", { noremap = true, silent = true })
49
50function ShowCopilotChatActionPrompt()
51 local actions = require("CopilotChat.actions")
52 require("CopilotChat.integrations.telescope").pick(actions.prompt_actions())
53end
54
55vim.api.nvim_set_keymap(
56 "n",
57 "<leader>0",
58 "<cmd>lua ShowCopilotChatActionPrompt()<cr>",
59 { noremap = true, silent = true }
60)
上記のうちprompts
に続く日本語文は、デフォルトでは英語で用意されている定型文を書き換えたものとなる。Copilot Chatは質問文と同じ言語で回答が返ってくるため、日本語で回答が欲しければ日本語で質問しなければならない。
ShowCopilotChatActionPrompt
ではtelescope.nvimを呼び出す関数が定義されている。この機能のなにが嬉しいのかというとコーディング中に頻繁に用いるであろう定型文をキーマップで即時に呼び出せるところだ。質問文が固定されていると回答も一意に定まりやすい。仮に定型文が100個くらいに増えてもtelescope.nvim由来の絞り込みで容易に対応できる。
他方、自由記述の入力はCopilotChatBuffer
によって呼び出される。現在のバッファを対象に具体的な質問を行うことで、より高度な提案を引き出す効果が期待できる。ただし、Copilotはあくまでコーディング支援用のツールなので天気や時事問題、文章の校正などについて尋ねても回答は得られない。
有効な質問を入力すると上記の形式で回答が得られる。期待以上に的を射た内容が多く、これがエディタ上で即時に使えて月額10ドルなら十分割に合うと感じた。業務内のコードをむやみに読み取らせるのは下手をするとインシデントに発展しかねないが、工夫次第では活用できる見込みが高い。
ホビーユースにおいて成果物自体が目的の場合には、質問とコピペを繰り返しながら制作を進めるアプローチも今時は考えられるだろう。専門技術を積んでいなくとも個々人の需要に適ったものが手に入るのはまさしく望ましい進歩の在り方である。
あるいは勉強目的ですら、極めて短期間のうちに有機的なトライアンドエラーを繰り返せるという意味では、昔の写経プログラミングより効率的に学習効果が得られるかもしれない。いずれにしても、便利な道具の使い方を知っておいて損はない。