0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Neovim v0.11におけるLSPの設定

Last updated at Posted at 2025-09-18

はじめに

最近Neovimを使い始めまして、その際にLSPの設定を行いました。VSCodeでは標準で実装されているLSPですが、自分で1から設定するとなるとなかなかに大変だったので、苦労を忘れないために記事に残しておこうと思います。LSPは一度設定するとしばらく触ることのないので、アウトプットしておかないと忘れそうですし。

LSPについて

LSPとは

LSP=Language Server Protocolの略称です。

Languageはプログラミング言語のことを指しています。各種プログラミング言語に対応したサーバーをLanguage Serverと呼び、そのサーバーと通信する際のプロトコルをLSPといいます。LSPに従って通信するクライアントはエディタ・IDEです(Vim, VSCode, IntelliJなど)。

例えば、VSCodeでindex.tsファイルを編集するとTypeScriptの文法に応じた自動補完やシンタックスエラーの表示や関数の定義ジャンプなどがデフォルトの機能として動作しますが、これらの機能はVSCodeとTypeScript用のLanguage ServerとのLSPに基づく通信がバックグラウンドで実行されていることによって実現しています。

また、LSPはMicrosoftが開発しておりJSON-RPCベースで動作します。

余談:JSON-RPCとは

  • RPC
    • Remote Procedure Call
    • リモートサーバーで定義された手続き(procedure)をクライアントから実行するプロトコル
  • JSON-RPC
    • その名の通り、JSONを用いたRPC
    • procedureの実行指示をJSON形式で記述してリモートサーバーに送信する。実行結果もJSONで返却される
    • 例:足し算を実行するaddというprocedureを実行するサンプルは以下の通り
request
{
  "jsonrpc": "2.0",
  "method": "add",
  "params": [5, 3],
  "id": 1
}
response
{
  "jsonrpc": "2.0",
  "result": 8,
  "id": 1
}

あわせて読みたい↓

Neovim built-in LSP

ではNeovimにおけるLSPはどうなっているのかというと、NeovimはデフォルトでLSPクライアントとしての機能を持っています。つまりプラグイン無しでもLSPの機能を利用することができます。

https://neovim.io/doc/user/lsp.html

Nvim supports the Language Server Protocol (LSP), which means it acts as a client to LSP servers and includes a Lua framework vim.lsp for building enhanced LSP tools.

自分はNeovim v0.11から使い始めた民なので以前のバージョンのことは想像でしか話せませんが、過去のNeovimバージョンではプラグインを用いてLSPを利用する方法がデファクトのようでした。ただ、v0.11の前後でLSPに関する設定ファイルの書き方や設定ファイルの配置の仕方が変化しており、ネットや生成AIで調べる際はどのバージョンに則った説明をしているかを注意深く確認する必要があります。(ググってHITする記事は大体0.11以前の書き方を紹介していることが多い印象です)

NeovimにおけるLSPの設定

私のNeovimの設定ファイルはこちらのリポジトリで管理しています。(LSPとは関係ないファイルも混ざっていますが)

LSPに関連するファイルはこの辺りです↓

※LSPに関係のあるファイルだけ抜粋

├── init.lua
└── lua
    └── lsp
        ├── init.lua
        └── ts_ls.lua
設定ファイルの全体像
.
├── init.lua
├── lazy-lock.json
└── lua
    ├── config
    │   └── lazy.lua
    ├── lsp
    │   ├── init.lua
    │   ├── lua_ls.lua
    │   ├── rust-analyzer.lua
    │   ├── tailwindcss.lua
    │   └── ts_ls.lua
    └── plugins
        ├── autopairs.lua
        ├── bufferline.lua
        ├── coc.lua
        ├── comment.lua
        ├── gitsigns.lua
        ├── lualine.lua
        ├── neoscroll.lua
        ├── nvim-tree.lua
        ├── nvim-web-devicons.lua
        ├── telescope.lua
        ├── treesitter.lua
        └── vim-illuminate.lua
  • init.lua・・・Neovim自体の初期設定
  • lsp/init.lua・・・LSPの初期設定
  • lsp/ts_ls.lua・・・各言語のLanguage Serverの設定

LSPの導入手順

LSPの導入手順をおおまかに説明します。以降の手順ではTypeScriptのLSPを設定する例を書きます。

1. lsp/init.luaの作成

まずは各言語のLSPで共通する設定を記述するためのlsp/init.luaというファイルを作成します。
後述するLspAttachの設定をここに書きます。
https://github.com/tttol/dotfiles/blob/main/nvim/lua/lsp/init.lua

2. lsp/ts_ls.luaの作成

TypeScript向けの設定を書くためにlsp/ts_ls.luaというファイルを作成します。
このファイルは以下のnvim-lspconfigというリポジトリから拝借したものです。

このリポジトリにはTypeScript以外にもさまざまな言語向けのファイルがPUSHされています。特にこだわりがない場合はここにあるファイルをそのまま利用するのが良さそうです。

nvim-lspconfigのlsp/配下のファイルを動的に取得する方法もあるようです。が、私はまだ試せてません。

3. init.luaでlsp/init.luaを読み込む

init.luaに以下の記述を追記します。これにより、lsp/init.luaの内容をNeovimが読み込んでくれるようになります。

require('lsp')

4. TypeScript向けのLanguage Serverのインストール

Neovimの設定ファイルの準備ができたら、最後にLanguage Serverをインストールします。TypeScriptの場合は以下のコマンドでインストールが可能です。

npm install -g typescript typescript-language-server

Language Serverのインストール方法はnvim-lspconfigのlsp/配下のファイルのコメント部分に記載されていることが多いです。ts_ls.luaのコメントにも以下の通り記述があります。

--- `typescript-language-server` depends on `typescript`. Both packages can be installed via `npm`:
--- ```sh
--- npm install -g typescript typescript-language-server
--- ```

手順4まで実施したのち、Neovimを再起動して任意の.tsファイルをNeovimで開くと定義ジャンプや自動補完が動作するはずです。

各種記載の解説

先ほどの導入手順の中で作成したlsp/init.luaの中身を詳しく見ていきます。

LspAttach

NeovimとLanguage Serverの接続が確立されたタイミングで、LspAttachが実行されます。LspAttachでは各言語で共通する設定を記述しています。(言語ごとの個別の設定もLspAttachで記述可能です)

具体的には以下のように記述します。

vim.api.nvim_create_autocmd('LspAttach', {
    callback = function(args)
        -- (略)
    end
})

autocmdとは

特定のイベントが発生した時に自動で実行されるコマンドのことです。autocmdはLSP以外の文脈でも使われているVimの機能です。

on_attach vs LspAttach

LspAttachとは別にon_attachというトリガーも存在します。両者はLSPクライアントがLanguage Serverに接続された時に実行されるという点では同じですが、実装方法が異なります。on_attachは各言語ごとに設定を記述する必要がありますが、LspAttachは一箇所に記述するだけでOKです。

on_attach
-- 各LSPサーバーの設定時に個別に指定
local my_on_attach = function(client, bufnr)
  -- 設定
end

-- サーバーごとに指定が必要
require('lspconfig').tsserver.setup({
  on_attach = my_on_attach,  -- ← ここで指定
})

require('lspconfig').pyright.setup({
  on_attach = my_on_attach,  -- ← ここでも指定
})

require('lspconfig').rust_analyzer.setup({
  on_attach = my_on_attach,  -- ← ここでも指定
})
LspAttach
-- 一度定義すれば、全てのLSP接続に自動適用
vim.api.nvim_create_autocmd('LspAttach', {
  callback = function(args)
    -- この設定が全LSPサーバーに自動適用される
  end,
})

他にも違いはあります。詳細は以下のリンクが参考になります。
https://zenn.dev/ryoppippi/articles/8aeedded34c914

※自分はLspAttachをメインで使っているのでon_attachの上手い使い方をわかっておらず…

自動補完

自動補完とは文字を打った後に自動で入力候補を表示する機能のことです。
image.png

公式リファレンスに記載があるので、この通りに設定していけばOKです。
https://neovim.io/doc/user/lsp.html#_lua-module:-vim.lsp.completion

ただし、-- Optional: trigger autocompletion on EVERY keypress. May be slow!の部分のコメントアウトは外す必要があります。

lsp/init.lua
lsp/init.lua
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))

        if client:supports_method('textDocument/completion') then
            if client.server_capabilities.completionProvider then
                -- Optional: trigger autocompletion on EVERY keypress. May be slow!
                local chars = {}; for i = 32, 126 do table.insert(chars, string.char(i)) end
                client.server_capabilities.completionProvider.triggerCharacters = chars

                -- Extend existing trigger characters
                local existing_chars = client.server_capabilities.completionProvider.triggerCharacters or {}
                local additional_chars = { '.', ':', '->', '::', '(', '[', '{', ' ' }
                for _, char in ipairs(additional_chars) do
                    table.insert(existing_chars, char)
                end
                client.server_capabilities.completionProvider.triggerCharacters = existing_chars
            end

            vim.lsp.completion.enable(true, client.id, args.buf, {
                autotrigger = true,
                convert = function(item)
                    return { abbr = item.label:gsub('%b()', '') }
                end,
            })
        end
    end,
})

キー配置の変更

コーディングをする上でよく利用する機能に対してキーマップを設定します。
例えば、

  • Go Definition(定義ジャンプ)=gd
  • Go Reference(参照)=gr
    などです。
lsp/init.lua
lsp/init.lua
-- LSP attach autocmd for common configuration
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))

        -- Common keymaps
        local opts = { buffer = args.buf, noremap = true, silent = true }
        vim.keymap.set("n", "<leader>e", vim.diagnostic.open_float,
            vim.tbl_extend("force", opts, { desc = "Show diagnostics" }))
        vim.keymap.set("n", "gd", vim.lsp.buf.definition, vim.tbl_extend("force", opts, { desc = "Jump to definition" }))
        vim.keymap.set("n", "gD", vim.lsp.buf.declaration,
            vim.tbl_extend("force", opts, { desc = "Jump to declaration" }))
        vim.keymap.set("n", "gi", vim.lsp.buf.implementation,
            vim.tbl_extend("force", opts, { desc = "Jump to implementation" }))
        vim.keymap.set("n", "gr", vim.lsp.buf.references, vim.tbl_extend("force", opts, { desc = "Show references" }))
        vim.keymap.set("n", "gt", vim.lsp.buf.type_definition,
            vim.tbl_extend("force", opts, { desc = "Jump to type definition" }))
        vim.keymap.set("n", "<leader>rn", vim.lsp.buf.rename, vim.tbl_extend("force", opts, { desc = "Rename symbol" }))
        vim.keymap.set({ "n", "v" }, "<leader>ca", vim.lsp.buf.code_action,
            vim.tbl_extend("force", opts, { desc = "Code action" }))

        -- Tab completion keymaps
        vim.keymap.set("i", "<Tab>", function()
            if vim.fn.pumvisible() == 1 then
                return "<C-n>"
            else
                return "<Tab>"
            end
        end, vim.tbl_extend("force", opts, { expr = true, desc = "Tab completion" }))

        vim.keymap.set("i", "<S-Tab>", function()
            if vim.fn.pumvisible() == 1 then
                return "<C-p>"
            else
                return "<S-Tab>"
            end
        end, vim.tbl_extend("force", opts, { expr = true, desc = "Shift-Tab completion" }))

        vim.keymap.set("i", "<CR>", function()
            if vim.fn.pumvisible() == 1 then
                return "<C-y>"
            else
                return "<CR>"
            end
        end, vim.tbl_extend("force", opts, { expr = true, desc = "Enter completion confirm" }))

        vim.opt_local.pumheight = 10 -- Limit popup menu height
    end,
})

        
-- Force override Tab completion for all filetypes after all plugins load
vim.api.nvim_create_autocmd('FileType', {
    pattern = '*',
    callback = function(args)
        vim.schedule(function()
            local opts = { buffer = args.buf, noremap = true, silent = true }
            vim.keymap.set("i", "<Tab>", function()
                if vim.fn.pumvisible() == 1 then
                    return "<C-n>"
                else
                    return "<Tab>"
                end
            end, vim.tbl_extend("force", opts, { expr = true, desc = "LSP Tab completion override" }))

            vim.keymap.set("i", "<S-Tab>", function()
                if vim.fn.pumvisible() == 1 then
                    return "<C-p>"
                else
                    return "<S-Tab>"
                end
            end, vim.tbl_extend("force", opts, { expr = true, desc = "LSP Shift-Tab completion override" }))

            -- vim.keymap.set("i", "<CR>", function()
            --     if vim.fn.pumvisible() == 1 then
            --         return "<C-y>"
            --     else
            --         return "<CR>"
            --     end
            -- end, vim.tbl_extend("force", opts, { expr = true, desc = "LSP Enter completion override" }))
        end)
    end,
})

最後の-- Force override Tab completion for all filetypes after all plugins load以降のところはちょっと力技だと思っていて、もっとスマートな書き方があるかなと思ってます…。自動補完で表示された候補を決定する際のキーマップを変更したくて、をTabに、をShift+Tabに、をEnterに上書きしています。ただ、設定の読み込み順序の問題なのかLspAttachに記載しても上書きされなかったので、lsp/init.luaの末尾にFileTypeのautocmd記載することで無理やり上書きを行なっています。

さいごに

自分のNeovimの設定ファイルはまだまだ発展途上で不完全なものなので、今後も改修していく予定です。この記事がこれからNeovimやLSPに入門する人の参考になれば幸いです。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?