これまで VSCode を使ってきましたが、前から気になっていた Neovim を使ってみました。
この記事では、実際に使っている設定ファイルをもとに、どんなプラグインを選んで、なぜそう設定したのかを紹介します。
ディレクトリ構成
~/.config/nvim/
├── init.lua # エントリーポイント
├── lazy-lock.json # プラグインのバージョンロックファイル
└── lua/
├── basic_config.lua # 基本設定
├── clipboard_config.lua # クリップボード設定
├── setup_plugin.lua # プラグインマネージャ初期化
└── plugins/
├── colorscheme.lua # カラーテーマ
├── copilot.lua # GitHub Copilot
├── git.lua # Git 連携
├── lsp.lua # LSP・補完
├── nvim_tree.lua # ファイルツリー
├── rest.lua # REST クライアント
└── telescope.lua # ファジーファインダー
init.lua はシンプルに 3 つのモジュールを読み込むだけです。
require('basic_config')
require('clipboard_config')
require('setup_plugin')
プラグインマネージャ: lazy.nvim
プラグイン管理には lazy.nvim を採用しています。
初回起動時に自動でダウンロードするブートストラップコードを setup_plugin.lua に書いておくことで、git clone 直後でも即座に環境が整います。
local lazypath = vim.fn.stdpath('data') .. '/lazy/lazy.nvim'
if not (vim.uv or vim.loop).fs_stat(lazypath) then
vim.fn.system({
'git', 'clone', '--filter=blob:none', '--branch=stable',
'https://github.com/folke/lazy.nvim.git', lazypath,
})
end
vim.opt.rtp:prepend(lazypath)
require('lazy').setup('plugins')
lua/plugins/ 以下にファイルを置くだけでプラグインが自動認識されるため、設定を機能ごとにきれいに分割できるのが気に入っています。
基本設定 (basic_config.lua)
地味ですが、ここがエディタの使い心地の土台になります。
インデント
opt.tabstop = 2
opt.softtabstop = 2
opt.shiftwidth = 2
opt.expandtab = true -- タブをスペースに展開
opt.autoindent = true
opt.smartindent = true
行番号
相対行番号と絶対行番号を同時に表示します。j/k での移動量が一目でわかるので非常に便利です。
opt.relativenumber = true
opt.number = true
不可視文字の可視化
タブ・スペース・改行などを記号で表示します。意図しない空白に気づけるのでコードレビュー時に重宝します。
opt.list = true
opt.listchars = {
tab = '▸ ',
trail = '-',
space = '.',
nbsp = '+',
eol = '$',
extends = '>',
precedes = '<',
}
その他
opt.ambiwidth = 'double': 「※」などの全角記号が半角文字と重ならないように設定opt.cursorline = true: カーソル行をハイライトopt.swapfile = false: スワップファイルを作成しないopt.termguicolors = true: 24bit カラー有効化
クリップボード設定 (clipboard_config.lua)
macOS / WSL / Linux の 3 環境を自動判別して適切なクリップボードバックエンドを設定しています。
local uname = vim.loop.os_uname().sysname
local is_wsl = vim.fn.has("wsl") == 1
if is_wsl then
vim.g.clipboard = {
name = "win32yank-wsl",
copy = { ["+"] = "win32yank.exe -i --crlf", ["*"] = "win32yank.exe -i --crlf" },
paste = { ["+"] = "win32yank.exe -o --lf", ["*"] = "win32yank.exe -o --lf" },
}
elseif uname == "Darwin" then
vim.g.clipboard = {
name = "macOS-clipboard",
copy = { ["+"] = "pbcopy", ["*"] = "pbcopy" },
paste = { ["+"] = "pbpaste", ["*"] = "pbpaste" },
}
else
vim.g.clipboard = {
name = "xclip",
copy = { ["+"] = "xclip -selection clipboard", ["*"] = "xclip -selection primary" },
paste = { ["+"] = "xclip -selection clipboard -o", ["*"] = "xclip -selection primary -o" },
}
end
これにより同じ dotfiles を複数のマシンで使い回せます。
カラーテーマ: iceberg.vim
iceberg.vim を使っています。
落ち着いた青みがかったダークテーマで、長時間作業しても目が疲れにくいのが選んだ理由です。
return {
'cocopon/iceberg.vim',
lazy = false,
priority = 1000, -- 最優先で読み込む
config = function()
vim.cmd [[ colorscheme iceberg ]]
end,
}
ファイルツリー: nvim-tree.lua
nvim-tree.lua でサイドバーにファイルツリーを表示しています。
Neovim 起動時に自動でツリーを開くように設定しています。
require("nvim-tree.api").tree.toggle(false, true) -- 起動時に自動表示
主なキーマッピングはプラグイン組み込みのものをそのまま使っています。
| キー | 操作 |
|---|---|
<leader>e | ファイルツリーにフォーカス |
a | ファイル作成(末尾 / でディレクトリ) |
d | ファイル削除 |
r | ファイル名変更 |
ファジーファインダー: Telescope
Telescope はファイル検索・grep・Git 操作など、あらゆる「選択」操作を担います。
-- ファイル・テキスト検索
vim.keymap.set('n', '<leader>ff', builtin.find_files, { desc = 'ファイル検索' })
vim.keymap.set('n', '<leader>fg', builtin.live_grep, { desc = 'テキスト検索 (grep)' })
vim.keymap.set('n', '<leader>fw', builtin.grep_string, { desc = 'カーソル下の単語を検索' })
vim.keymap.set('n', '<leader>fr', builtin.oldfiles, { desc = '最近開いたファイル' })
-- バッファ・ヘルプ
vim.keymap.set('n', '<leader>fb', builtin.buffers, { desc = 'バッファ一覧' })
vim.keymap.set('n', '<leader>fh', builtin.help_tags, { desc = 'ヘルプ検索' })
-- Git
vim.keymap.set('n', '<leader>gc', builtin.git_commits, { desc = 'Git コミット履歴' })
vim.keymap.set('n', '<leader>gs', builtin.git_status, { desc = 'Git ステータス' })
-- 診断
vim.keymap.set('n', '<leader>fd', builtin.diagnostics, { desc = '診断 (エラー/警告)' })
node_modules と .git/ は検索対象から除外し、隠しファイルは含めるよう設定しています。
require('telescope').setup {
defaults = {
file_ignore_patterns = { 'node_modules', '.git/' },
},
pickers = {
find_files = {
hidden = true,
},
},
}
LSP と自動補完 (lsp.lua)
Go を主に書くため、gopls を中心に LSP 環境を整えました。
gopls の設定には Neovim 0.11 で導入された vim.lsp.config / vim.lsp.enable を使っています。
使用プラグイン
| プラグイン | 役割 |
|---|---|
neovim/nvim-lspconfig | LSP クライアント設定 |
hrsh7th/nvim-cmp | 自動補完エンジン |
hrsh7th/cmp-nvim-lsp | LSP を補完ソースとして追加 |
L3MON4D3/LuaSnip | スニペットエンジン |
LSP キーマッピング
vim.keymap.set('n', 'gd', vim.lsp.buf.definition, { desc = '定義へジャンプ' })
vim.keymap.set('n', 'gr', vim.lsp.buf.references, { desc = '参照一覧' })
vim.keymap.set('n', 'K', vim.lsp.buf.hover, { desc = 'ホバー情報' })
vim.keymap.set('n', '<leader>rn', vim.lsp.buf.rename, { desc = 'リネーム' })
vim.keymap.set('n', '<leader>ca', vim.lsp.buf.code_action, { desc = 'コードアクション' })
vim.keymap.set('n', '<leader>F', function() vim.lsp.buf.format({ async = true }) end, { desc = 'フォーマット' })
gopls の設定
completeUnimported で未 import パッケージも補完候補に出し、diagnosticsDelay をデフォルトの 250ms から 100ms に短縮して診断の表示を早めています。
vim.lsp.config('gopls', {
cmd = { 'gopls' },
filetypes = { 'go', 'gomod', 'gowork', 'gotmpl' },
root_markers = { 'go.work', 'go.mod', '.git' },
settings = {
gopls = {
analyses = { unusedparams = true, shadow = true },
staticcheck = true,
completeUnimported = true,
diagnosticsDelay = '100ms',
},
},
})
vim.lsp.enable('gopls')
Go 専用の工夫
保存時に goimports CLI を直接呼び出して import の整理とフォーマットを一括で行います。
LSP 経由(source.organizeImports コードアクション)も試しましたが、gopls の通信オーバーヘッドで保存が遅くなり、タイミングのずれから 2 回保存しないと反映されない問題がありました。手元の環境では CLI 直接呼び出しの方が高速かつ確実でした。
vim.api.nvim_create_autocmd('BufWritePre', {
pattern = '*.go',
callback = function()
local content = table.concat(vim.api.nvim_buf_get_lines(0, 0, -1, false), '\n')
local result = vim.fn.system({ 'goimports', '-srcdir', vim.fn.expand('%:p:h') }, content)
if vim.v.shell_error == 0 then
vim.api.nvim_buf_set_lines(0, 0, -1, false, vim.split(result, '\n', { plain = true }))
end
end,
})
gopls は補完候補を確定した時点では additionalTextEdits(import 文の自動追加など)をまだ返していないことがあります。そこで、nvim-cmp の confirm_done イベントで completionItem/resolve を LSP サーバに再問い合わせし、返ってきた編集を適用しています。
cmp.event:on('confirm_done', function(event)
local item = event.entry:get_completion_item()
if not vim.tbl_isempty(item.additionalTextEdits or {}) then return end
local source = event.entry.source.source
if not source or not source.client then return end
local client = source.client
client.request('completionItem/resolve', item, function(err, resolved)
if err or not resolved or vim.tbl_isempty(resolved.additionalTextEdits or {}) then return end
vim.lsp.util.apply_text_edits(resolved.additionalTextEdits, vim.api.nvim_get_current_buf(), client.offset_encoding)
end)
end)
また、BufWritePre で goimports CLI がバッファを書き換えた後、gopls が変更を認識するまでにラグがあり、保存直後に古い状態で診断が出ることがあります。これを回避するため、保存後に textDocument/didSave を gopls に通知して診断を即時再計算させています。ファイルを開いた直後にも同じ通知を行い、初回表示で診断が出るようにしています。
-- 保存後に診断を即時トリガー
vim.api.nvim_create_autocmd('BufWritePost', {
pattern = '*.go',
callback = function()
local clients = vim.lsp.get_clients({ bufnr = 0, name = 'gopls' })
for _, client in ipairs(clients) do
client.notify('textDocument/didSave', {
textDocument = { uri = vim.uri_from_bufnr(0) },
})
end
end,
})
-- ファイルを開いた直後にも診断をトリガー
vim.api.nvim_create_autocmd('LspAttach', {
group = vim.api.nvim_create_augroup('GoLspDiagOnOpen', {}),
pattern = '*.go',
callback = function(ev)
vim.defer_fn(function()
local clients = vim.lsp.get_clients({ bufnr = ev.buf, name = 'gopls' })
for _, client in ipairs(clients) do
client.notify('textDocument/didSave', {
textDocument = { uri = vim.uri_from_bufnr(ev.buf) },
})
end
end, 200) -- gopls のアタッチ完了を待つ
end,
})
Git 連携 (git.lua)
gitsigns.nvim — GitLens 風のインライン blame
gitsigns.nvim で変更差分の表示と行末 blame を実現しています。
current_line_blame = true,
current_line_blame_formatter = ' <author>, <author_time:%Y-%m-%d> - <summary>',
カーソルのある行の右端に You, 2024-01-15 - fix: hogehoge のように表示されます。
主なキーマッピング:
| キー | 操作 |
|---|---|
]c / [c | 次/前の変更箇所へ移動 |
<leader>hs | Hunk をステージ |
<leader>hr | Hunk をリセット |
<leader>hp | Hunk をプレビュー |
<leader>hb | blame 詳細表示 |
<leader>hd | 差分を表示 |
diffview.nvim — リッチな差分ビュー
diffview.nvim でブランチ間の差分やファイル履歴を確認できます。
vim.keymap.set('n', '<leader>dv', '<cmd>DiffviewOpen<CR>', { desc = 'Diffview を開く' })
vim.keymap.set('n', '<leader>do', '<cmd>DiffviewOpen develop..HEAD<CR>', { desc = 'develop..HEAD の差分' })
vim.keymap.set('n', '<leader>dh', '<cmd>DiffviewFileHistory %<CR>', { desc = '現在ファイルの履歴' })
develop..HEAD のショートカットがあると PR 前の確認が捗ります。
GitHub Copilot (copilot.lua)
copilot.lua で GitHub Copilot をネイティブに統合しています。
suggestion = {
enabled = true,
auto_trigger = true,
keymap = {
accept = '<C-l>', -- 全体を確定
accept_word = '<C-w>', -- 単語単位で確定
accept_line = '<C-j>', -- 1行単位で確定
next = '<M-]>', -- 次の候補
prev = '<M-[>', -- 前の候補
dismiss = '<C-]>', -- 却下
},
},
accept_word と accept_line を使い分けることで、提案の一部だけを取り込む操作が快適になります。
REST クライアント: rest.nvim
rest.nvim で .http ファイルから API リクエストを直接実行できます。
curl やブラウザの拡張機能、Postman等のツールを開かずに、エディタ内で API の動作確認ができるのが便利です。
vim.keymap.set('n', '<leader>rr', '<cmd>Rest run<CR>', { desc = 'Run REST request' })
vim.keymap.set('n', '<leader>rl', '<cmd>Rest last<CR>', { desc = 'Re-run last REST request' })
vim.keymap.set('n', '<leader>ro', '<cmd>Rest open<CR>', { desc = 'Open REST result pane' })
レスポンスは rest.nvim 側の整形フックを有効にしつつ、JSON バッファでは jq を formatprg に設定して、読みやすい形に整形しています。
vim.g.rest_nvim = {
request = {
skip_ssl_verification = true,
},
response = {
hooks = {
decode_url = true,
format = true,
},
},
}
vim.api.nvim_create_autocmd('FileType', {
pattern = 'json',
callback = function()
vim.opt_local.formatprg = 'jq .'
end,
})
まとめ
| カテゴリ | 採用プラグイン |
|---|---|
| プラグイン管理 | lazy.nvim |
| カラーテーマ | iceberg.vim |
| ファイルツリー | nvim-tree.lua |
| ファジーファインダー | Telescope |
| LSP / 補完 | nvim-lspconfig + nvim-cmp + LuaSnip |
| Git | gitsigns.nvim + diffview.nvim |
| AI 補完 | copilot.lua (GitHub Copilot) |
| REST クライアント | rest.nvim |
最初は「設定が大変そう」と思っていましたが、lazy.nvim のおかげでプラグインの追加・管理が非常に楽になっています。lua/plugins/ にファイルを置くだけで自動認識されるので、機能ごとに設定を分割でき、見通しも良いです。
gopls 周りでは goimports の実行方式(CLI vs LSP)や completionItem/resolve、textDocument/didSave の通知など、実際に使ってみて初めてわかるハマりどころがいくつかありました。こうした細かい調整を積み重ねることで、保存→診断→修正のサイクルがスムーズに回るようになっています。
VSCode からの移行で最も大きかったのは、設定の完全な透明性と制御です。
この記事で紹介した BufWritePre での goimports 直接呼び出しや completionItem/resolve の再問い合わせのように、あらゆるイベントに Lua で割り込み、LSP の内部挙動まで手を入れられます。VSCode は拡張機能 API の範囲内でしか動作できず、「拡張機能がサポートしていない」と詰まるケースがありますが、Neovim にその壁はありません。
設定がすべて Lua のテキストファイルであることも利点です。settings.json + GUI の VSCode と違い、より多くの設定を git で差分管理できます。
Neovim、ぜひ試してみてください。