👶

今からNeovimを始める人のLSP最短設定 (0.11, 2025-10-04現在)

に公開
2

巷にはNeovimのLSPの設定をまとめた記事が多くあり、この記事もその1つです。
しかし、既存の記事は情報が古かったり、既にLSPを設定したユーザー向けの移行ガイドであることが多いです。
この記事は、2025年10月に今からNeovimを始める方向けのガイドです。
いずれこの記事も昔のものとなりますが、現時点で最短でLSPの設定を行うための手順を解説します。

既に古いバージョンのNeovimを使っている人向けの移行記事はこちら:

事前知識

LSP関連設定

いよいよ設定を始めます。
ここではNeovimの設定を書くのに必要なLuaの言語サーバー(lua-language-server)の設定をすることにしましょう。事前に lua-language-server を手元にインストールしておいてください。
また、ここではできるだけ依存を減らしてシンプルな説明を試みるため、Neovimの標準APIとnvim-lspconfig(とプラグインマネージャー)のみを使った方法を解説します。
mason.nvimmason-lspconfig を使う方法は他の記事を参照ください。

ディレクトリ構造

Neovim標準のディレクトリ構造に従いましょう。~/.config/nvim/init.luaから始まります。

$ tree ~/.config/nvim/
.
├── after    # init.luaの読み込みが終わった後に自動で読み込まれる
│   └── lsp  # 言語サーバーの上書き設定が自動で読み込まれる
│       └── lua_ls.lua
├── init.lua # エントリーポイント
└── lua      # init.luaから読み込まれる
    ├── config
    │   ├── lazy.lua
    │   └── lsp.lua
    └── plugins
        └── nvim-lspconfig.nvim

参考: Lua-guide - Neovim docs
https://neovim.io/doc/user/lua-guide.html#lua-guide-modules

各ファイルの説明

以下では、各ファイルの中身を説明します。

init.lua では、lazy.nvim とLSP関連の設定ファイルを読み込みます。

init.lua
require("config.lazy")
require("config.lsp")

lua/config/lazy.lua では、 lazy.nvim の設定を行い、プラグイン設定の置き場所を決めます。
既に別の場所に設定している場合は都度読み替えてください。

lua/config/lazy.lua
-- lazy.nvimの設定
-- Ref: https://lazy.folke.io/installation
-- ...

-- Setup lazy.nvim
require("lazy").setup({
  spec = {
    -- lua/plugins/ 以下のluaファイルをlazy.nvimのPlugin Specとして自動で読み込む
    { import = "plugins" },
  },
})

一旦LSP関連の設定は飛ばし、先に nvim-lspconfig の設定を書きます。
先ほど設定した lua/plugins の下にPlugin Specを書くだけで lazy.nvim が自動でインストール、設定をしてくれます。

lua/plugins/nvim-lspconfig.lua
---@type LazyPluginSpec
return {
  "neovim/nvim-lspconfig",
  -- Bufferが読み込まれたときやファイルが作成されたときに遅延ロードする
  event = { "BufReadPre", "BufNewFile" },
}

nvim-lspconfig の設定をしたので、一旦飛ばしたLSP関連の設定をします。

lua/config/lsp.lua
vim.lsp.enable({
  -- nvim-lspconfig で"lua_ls"という名前で設定したプリセットが読まれる
  -- https://github.com/neovim/nvim-lspconfig/blob/master/lsp/lua_ls.lua
  "lua_ls",
  -- 他の言語サーバーの設定
  -- "gopls",
})

-- 言語サーバーがアタッチされた時に呼ばれる
vim.api.nvim_create_autocmd("LspAttach", {
  group = vim.api.nvim_create_augroup("my.lsp", {}),
  callback = function(args)
    local client = assert(vim.lsp.get_client_by_id(args.data.client_id))
    local buf = args.buf

    -- デフォルトで設定されている言語サーバー用キーバインドに設定を追加する
    -- See https://neovim.io/doc/user/lsp.html#lsp-defaults
    -- 言語サーバーのクライアントがLSPで定められた機能を実装していたら設定を追加するという流れ

    if client:supports_method("textDocument/definition") then
      vim.keymap.set("n", "gd", vim.lsp.buf.definition, { buffer = buf, desc = "Go to definition" })
    end

    if client:supports_method("textDocument/hover") then
      vim.keymap.set("n", "<leader>k",
        function() vim.lsp.buf.hover({ border = "single" }) end,
        { buffer = buf, desc = "Show hover documentation" })
    end

    -- Enable auto-completion. Note: Use CTRL-Y to select an item. |complete_CTRL-Y|
    if client:supports_method("textDocument/completion") then
      -- Optional: trigger autocompletion on EVERY keypress except brackets. May be slow!
      local chars = {}
      for i = 32, 126 do
        local c = string.char(i)
        if c ~= "(" and c ~= ")" and c ~= "[" and c ~= "]" and c ~= "{" and c ~= "}" then
          table.insert(chars, c)
        end
      end
      client.server_capabilities.completionProvider.triggerCharacters = chars
      vim.lsp.completion.enable(true, client.id, args.buf, { autotrigger = true })
    end

    -- Auto-format ("lint") on save.
    -- Usually not needed if server supports "textDocument/willSaveWaitUntil".
    if not client:supports_method("textDocument/willSaveWaitUntil")
        and client:supports_method("textDocument/formatting") then
      vim.api.nvim_create_autocmd("BufWritePre", {
        group = vim.api.nvim_create_augroup("my.lsp", { clear = false }),
        buffer = args.buf,
        callback = function()
          vim.lsp.buf.format({ bufnr = args.buf, id = client.id, timeout_ms = 1000 })
        end,
      })
    end

    if client:supports_method("textDocument/inlineCompletion") then
      vim.lsp.inline_completion.enable(true, { bufnr = buf })
      vim.keymap.set("i", "<Tab>", function()
        if not vim.lsp.inline_completion.get() then
          return "<Tab>"
        end
        -- close the completion popup if it's open
        if vim.fn.pumvisible() == 1 then
          return "<C-e>"
        end
      end, {
        expr = true,
        buffer = buf,
        desc = "Accept the current inline completion",
      })
    end
  end,
})

ここまででLua言語サーバーの設定は完了しました。
この状態でNeovimを起動してLuaファイルのシンボルにカーソルを当て、 <leader>k<leader>はデフォルトではスペースキー)を押すと言語サーバーから提供された情報が見れるのではないでしょうか。
これも nvim-lspconfig がLua言語サーバー用のプリセットを提供してくれているおかげです。

しかし、Lua言語サーバーはVim/Neovimに特化したものではないため、グローバルで使用している vim.lsp.enablevim キーワードの情報がなく警告を出してくるはずです。
最後に、この警告を消す設定をしましょう。

after/ ディレクトリは特別なディレクトリで、通常の読み込みプロセスが終わった後に読み込まれます。
after/lsp/ ディレクトリの下で言語サーバーの設定を返すことでNeovimが自動でこの設定を呼んでくれます。
nvim-lspconfig は本来ここに書くべき各言語サーバー用の設定を予め書いてくれているものなのです。

グローバルの vim キーワードは Neovim に同梱されたLuaファイルで定義されています。
このLuaファイルを格納しているディレクトリには、 vim.env.VIMRUNTIME .. "/lua" でアクセスできます。
例えばHomebrewでNeovimをインストールしていればこれは /opt/homebrew/share/nvim/runtime/lua などを指すはずです。

after/lsp/lua_ls.lua を作成し、 settings.Lua.workspace.library の配列に vim.env.VIMRUNTIME .. "/lua" を追加することで、vim キーワードの定義を読み込むことができ、警告を消すことができます。

after/lsp/lua_ls.lua
---@type vim.lsp.Config
return {
  settings = {
    Lua = {
      workspace = {
        library = {
          vim.env.VIMRUNTIME .. "/lua",
        },
      },
    },
  },
}

これで無事警告を消すことができました。

終わりに

良いNeovimライフを!

Discussion

あきもあきも

globalsの設定で警告を消すのも良いですが、Neovimに同梱されている型定義を読み込ませるとより正確な情報が得られてハッピーかもしれません

---@type vim.lsp.Config
return {
  settings = {
    Lua = {
      workspace = {
        library = {
          "/opt/homebrew/share/nvim/runtime/lua",
        },
      },
    },
  },
}

100%定義されているわけではないので、一部警告は出てしまいますが……

ras0qras0q

確かに、"/opt/homebrew/share/nvim/runtime/lua" (これは vim.env.VIMRUNTIMEでbrewの依存を剥がせそうです) でグローバルの vim が型定義されているのでそっちを使うのが良さそうですね。
実は自分の手元ではそれも併せて設定しているんですが、ここでは最小限のカスタムを行うために settings.Lua.diagnostics.globals だけを書いていました。
修正しておきます、ご指摘ありがとうございます!