COMPANY SERVICE STAFF BLOG NEWS CONTACT
2026.02.27

Neovim を使ってみた

テクログdevelopment

これまで 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-lspconfigLSP クライアント設定
hrsh7th/nvim-cmp自動補完エンジン
hrsh7th/cmp-nvim-lspLSP を補完ソースとして追加
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-cmpconfirm_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)

また、BufWritePregoimports 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>hsHunk をステージ
<leader>hrHunk をリセット
<leader>hpHunk をプレビュー
<leader>hbblame 詳細表示
<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_wordaccept_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 バッファでは jqformatprg に設定して、読みやすい形に整形しています。

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
Gitgitsigns.nvim + diffview.nvim
AI 補完copilot.lua (GitHub Copilot)
REST クライアントrest.nvim

最初は「設定が大変そう」と思っていましたが、lazy.nvim のおかげでプラグインの追加・管理が非常に楽になっています。
lua/plugins/ にファイルを置くだけで自動認識されるので、機能ごとに設定を分割でき、見通しも良いです。

gopls 周りでは goimports の実行方式(CLI vs LSP)や completionItem/resolvetextDocument/didSave の通知など、実際に使ってみて初めてわかるハマりどころがいくつかありました。こうした細かい調整を積み重ねることで、保存→診断→修正のサイクルがスムーズに回るようになっています。

VSCode からの移行で最も大きかったのは、設定の完全な透明性と制御です。
この記事で紹介した BufWritePre での goimports 直接呼び出しや completionItem/resolve の再問い合わせのように、あらゆるイベントに Lua で割り込み、LSP の内部挙動まで手を入れられます。VSCode は拡張機能 API の範囲内でしか動作できず、「拡張機能がサポートしていない」と詰まるケースがありますが、Neovim にその壁はありません。

設定がすべて Lua のテキストファイルであることも利点です。settings.json + GUI の VSCode と違い、より多くの設定を git で差分管理できます。

Neovim、ぜひ試してみてください。

この記事を書いた人

Sieg

入社年
2021年
出身地
大阪府
業務内容
WEB開発
特技・趣味
ゲーム、読書

テクログに関する記事一覧