NeovimでLLMを動かすcodecompanion.nvimの使い方

この記事はVim駅伝の2024年8月30日の記事です。
前回(2024年8月28日)の記事はteramakoさんのNeovim nvim-cmp の補完ソースプラグインと gin.vim の action 補完ソースを書いた話でした。


今回はLLMをNeovimで使えるようにするcodecompanion.nvimを紹介します。

codecompanion.nvimとは

LLMを使ってNeovim上でチャットしたり、指示内容に合わせてバッファを書き換えるプラグインです。
記事執筆時(2024年8月28日)ではOllama、OpenAI、Copilot、Gemini、Anthropicをサポートしています。

プロンプトを渡す際に、間にLuaの処理を挟むことで柔軟に定義できます。
たとえば、「LSPのdiagnosticsをまとめてAIに解説してもらうプロンプト」がデフォルトで定義されています(上記の動画)。

導入方法

curlが必要です。Neovimのバージョンは0.92以上のみ対応しています。

プラグインのインストール

パッケージマネージャーでインストールします。

リポジトリ名だけのコピペ用
olimorris/codecompanion.nvim
lazy.nvimの例
{
"olimorris/codecompanion.nvim",
dependencies = {
"nvim-lua/plenary.nvim",
"nvim-treesitter/nvim-treesitter",
"nvim-telescope/telescope.nvim", -- Optional
{
"stevearc/dressing.nvim", -- Optional: Improves the default Neovim UI
opts = {},
},
},
-- config = ...
},

いくつか依存がありますが、Optionalとあるとおりtelescopeとdressingは無くても動きます。

モデルの導入

各自動かしたいモデルやAPIキーを用意します。

この記事では、CodeCompanionで動かすモデルとしてOllamacodellama:7bを使います。
次のコマンドでモデルをダウンロードできます。

Terminal window
ollama run codellama:7b

まずは簡単な設定

用意したモデルを使うための最低限の設定をしていきます。

adapters

adaptersで使用するLLMのツールやモデルを指定します。

require("codecompanion").setup({
adapters = {
ollama = function()
return require("codecompanion.adapters").extend("ollama", {
schema = {
model = {
default = "codellama:7b",
},
},
})
end,
},
-- ...
})

OpenAIのAPIキーやOllamaのリモートサーバーのURLもadaptersで指定できます。

strategies

strategiesにどの機能でどのadapterを使うか指定します。

require("codecompanion").setup({
-- ...
strategies = {
chat = {
adapter = "ollama",
},
inline = {
adapter = "ollama",
},
agent = {
adapter = "ollama",
},
},
})

チャット機能ではchatに指定したadapterが使われます。
:CodeCompanion 〇〇のようなチャット以外ではinlineのadapterが使われます。

agentは後で説明します。

短縮入力の設定

CodeCompanionのコマンド名はCodeCompanionから始まります。

コマンド名が少し長いため、次のように設定しておくと楽です。

vim.cmd([[cab cc CodeCompanion]])

コマンドラインでccSpaceを入力したらCodeCompanionに展開されます。

使い方

CodeCompanionの機能とコマンドを紹介します。

CodeCompanionChat

:CodeCompanionChatを実行すると、チャット専用バッファが開きます。最初はノーマルモードで開きます。

チャットの最初の画面

質問を入力後、Enterを押すとLLMに送信され、回答が始まります。

※モデルやPCのスペックによっては回答の開始までに時間がかかります。「回答中」をわかりやすくする設定は後で説明します。

チャット専用バッファ

画像の回答の審議はさておき、適切にコードブロックがハイライトされていますね。
チャットは通常のバッファと同じように編集できるため、回答が冗長で見づらい場合は適宜削除できます。

チャット閉じるにはCtrl+cを入力します。

?でヘルプが開きます。

チャットのヘルプ

キーマップの他に、Variables(変数)が出てきましたね。

変数

チャットでは変数が指定できます。

たとえば#buffer:18-21と書けば、開いているバッファの18~21行目の内容に関連した質問ができます。

チャットで変数を使った例

変数は他にもいくつか定義されています。

変数名LLMに共有される内容
#buffer現在のバッファ
#buffer:X-Y現在のバッファのX~Y行
#buffers開いているすべてのバッファ
#editor見えている範囲の画面
#lsp現在のバッファのLSPの情報

バッファの指定範囲をチャットに送る

バッファの特定の範囲をLLMに共有する方法は、#buffer以外にもあります。

共有したい部分をビジュアルモードで選択してコマンド:'<,'>CodeCompanionAddを実行すれば、チャットに入力内容が挿入されます。

CodeCompanion

:CodeCompanion /hoge系のコマンドを実行すると、あらかじめ設定したプロンプトを実行できます。

/explain

:CodeCompanion /explainを実行すると、チャットで選択範囲の内容を説明してくれます

ノーマルモードでは現在の行のみが対象となってしまうため、:CodeCompanion /hoge系のコマンドではビジュアルモードで実行するとよいでしょう。

/tests

:CodeCompanion /testsを実行すると、選択範囲のテストコードを生成してくれます。コマンド名はtests(複数形)です。

testsの実行例

TypeScriptを使って試した所、jestが使われました。

実装を見てみると、プロンプトに「言語に合わせて適切なテストフレームワークを使ってね」と書かれています。

using an appropriate testing framework for the identified programming language

フレームワークを指定したい場合は、実装をコピペして独自のプロンプトを設定しましょう(後述)。

/fix

:CodeCompanion /fixを実行すると、選択範囲のコードの修正方法をチャットで解説してくれます。

/buffer

:CodeCompanion /buffer 〇〇を実行すると、指示内容に基づいて挙動が変化します。

リファクタリングしてほしい範囲を選択してから次のように実行すると、バッファが書き換わります

:'<,'>CodeCompanion /buffer refactor this

一方、次のようにチャットで答えてねと指示すると、バッファの内容に基づいてチャットで答えてくれます

:'<,'>CodeCompanion /buffer explain this. answer at chat

※ バッファを書き換えるのか、チャットで答えればいいのかを判断できずにエラーになることもしばしばあります。

/lsp

:CodeCompanion /lspを実行すると、選択範囲のdiagnosticsを自動でまとめて解説してくれます。

lspのdiagnosticsの解説例

diagnosticsのリスト化としても優秀です。

スラッシュなし

:CodeCompanion単体で実行すると、プロンプト画面が表示されます。

プロンプト画面

質問を入力後、その内容に応じてチャットで答えたりバッファを書き換えたりします。

Agents / Tools

外部ツールを呼び出してエージェントとして動かす機能もあります。@code_runner@rag@buffer_editorの3つがサポートされています。

たとえばDockerのコンテナでコードを実行するには、チャットでメンション風に@code_runnerを添えます。

@code_runner can you run some code for me?

筆者の環境ではうまく動かせていないため、動かせるようになったら別途記事を書きます。

Action Palette

:CodeCompanionActionsを実行すると、登録したプロンプトの一覧が表示され、選択すると実行できます。

Action Paletteの例

ビジュアルモードで実行すれば、前述の/lspなども一覧に入ってきます。

上記の画像はtelescope.nvimを使っています。telescope.nvim無しでも動きますが絞り込みはできません。

設定の詳細

最初に紹介した設定は最低限でした。ここからはもう少しカスタマイズした例を紹介します。

実行中にステータスラインへ反映

CodeCompanionの実行(というよりLLM周り)は時間がかかるため、ステータスラインに実行中であることを表示しておくと便利です。

動画の例では画面右下にCompanionという文字とスピナーを表示しています。

たとえばlualine.nvimの例は次のとおりです。別のファイルに書いてlualine側で呼び出しています。

lualine/cc-component.lua
local M = require("lualine.component"):extend()
M.processing = false
M.spinner_index = 1
local spinner_symbols = {
"⠋",
"⠙",
"⠹",
"⠸",
"⠼",
"⠴",
"⠦",
"⠧",
"⠇",
"⠏",
}
local spinner_symbols_len = 10
-- Initializer
function M:init(options)
M.super.init(self, options)
local group = vim.api.nvim_create_augroup("CodeCompanionHooks", {})
vim.api.nvim_create_autocmd({ "User" }, {
pattern = "CodeCompanionRequest*",
group = group,
callback = function(request)
if request.match == "CodeCompanionRequestStarted" then
self.processing = true
elseif request.match == "CodeCompanionRequestFinished" then
self.processing = false
end
end,
})
end
-- Function that runs every time statusline is updated
function M:update_status()
if self.processing then
self.spinner_index = (self.spinner_index % spinner_symbols_len) + 1
return "Companion " .. spinner_symbols[self.spinner_index]
else
end
end
return M
lualine/index.lua
require("lualine").setup({
-- ...
sections = {
-- ...
lualine_z = {
{ require("plug/lualine/cc-component") },
},
},
})

余談ですが、筆者がインストールした時はドキュメントが古かったため、OSSコントリビュートで更新しました。

独自のプロンプトを設定

default_promptsに設定することで、独自のプロンプトを定義できます。プロンプトごとにモデルも変更できます。

require("codecompanion").setup({
-- ...
default_prompts = {
["Hoge"] = {
strategy = "chat",
description = "foo bar",
opts = {
adapter = {
name = "ollama",
model = "elyza:jp8b",
},
},
prompts = {
{
role = "system",
content = "あなたは猫です。全部の語尾ににゃーとつけてください。",
},
{
role = "user",
content = "会話を始めます。",
},
},
},
},
})

promptsにプロンプトを書いていきます。

promptscontent文字列を返す関数を書いてもOKです。これでかなり柔軟なカスタマイズが可能です。NeovimのAPIを読んでごにょごにょできます。

興味のある人はRECIPES.mdデフォルトの設定の実装を読んでみると面白いかもしれません。

トラブルシューティング

随時追記します。

結果が出ない

次のようなメッセージが表示され、結果がどこにも反映されないことがあります。

Could not determine where to place the output from the prompt

メッセージのとおり、そもそも結果をどこに反映させればいいか判断できないことが原因です。

プロンプトで”answer at chat”や”replace”などの単語を含めましょう(それでもたまにうまくいかない)。

Noiceを使ってたのにUIの位置が変わってしまった

noice.nvimユーザーがいきなりdressing.nvimを入れると、UIが少し変わって困惑するかもしれません。

↓dressing.nvimのプロンプトの例。カーソル位置付近に表示されます。

dressing.nvimのプロンプトの例

↓noice.nvimのプロンプトの例。画面中央に表示されます。

noice.nvimのプロンプトの例

noice.nvimを優先させたい場合、dressing.nvimの設定を変えましょう。

{
"olimorris/codecompanion.nvim",
dependencies = {
"nvim-lua/plenary.nvim",
"nvim-treesitter/nvim-treesitter",
"nvim-telescope/telescope.nvim", -- Optional
{
"stevearc/dressing.nvim", -- Optional: Improves the default Neovim UI
opts = {
input = {
enabled = false,
},
},
},
},
-- config = ...
},

以上、CodeCompanionの紹介でした。

開発がかなり活発なようです。この記事を書いている間にも何回も更新されてました。