あなたもLSPにはまってみませんか？NeovimでLSP環境を作る！

お久しぶりです。まっちゃラテです🍵  
今回は、自分が興味で入れたLSP(Language Server Protocol)を、私のNeovimちゃんに導入して、すごくIDEチックになってうれしくなったので、我流ですが本記事にその方法をわかりやすくまとめてみました🖊

Neovimは長い間操っていますが、そんなに内部構造に詳しいわけではないです、、、  
なのでその辺を了承した上で読んでいただけると幸いです。

# 📒目次
- :question: LSPって？
- :computer:  **本題** : LSPの導入
- :fire:  もっとLSPの機能を堪能する
- :game_die: 動作確認
- :pencil: まとめ
- :exclamation: 付録: 実際に起きたエラーたち

### 導入環境
- **Windows 11 Home**
- **PowerShell ver 5.1.26100.4061**
- **Neovim ver 0.11.1**

# :question: LSPって？
LSPとは、Language Server Protocol の略称です。詳しい説明は[こちらの記事](https://zenn.dev/vim_jp/articles/40b2c348386f88)を参照すると良いと思われます。すごく具体的に書かれているので勉強になります。

ざっくりいうと、LSPは入力補完機能、定義ジャンプ、変数定義箇所の検索といったIDEに備わっている標準機能を、別の Language Server (言語サーバー)にエディタと分離して提供されるサービスのようなものです。

一方でこのサーバを使っているエディタ側は、サーバ間の通信を行います。この通信のやりとりの内容を定義したものをLSPと呼ばれています。その名の通り、プロトコルですね！

そのため、LSPをNeovimに導入することでIDEのような機能を使えるようになり、よりVSCodeチックになります。個人的には、特に入力補完がすごく優れていると思ったので、今回のLSP導入のきっかけとなりました :sound: 

現在ではLSPクライアントの種類はたくさんの数があり、これらがまとめられた文書ファイルとして [nvim-lspconfig/doc/configs.md](https://github.com/neovim/nvim-lspconfig/blob/master/doc/configs.md) が挙げられます。この中から、適宜使いたいクライアントを選んでインストールをします。  
また、インストールできるクライアントの一覧はNeovimのコマンドラインにおいて `:help lspconfig-all` と入力することでも見ることができます。

### LSP導入後のメリット・デメリット
前述した通り、LSPには様々なコードエディタを支援する機能が盛り込まれています。そのためコードを書く側の苦労が減ることが予想されます。実際、膨大なコードから様々なエラーを解析したり、修正したりすることはすごく手間がかかると思われます。(自分はあまり体験したことがないので大きく言えないのですが...)

そのため、LSPを適用することで素早くその箇所を抽出し、さらにコーディングを効率よく進められることが、利点として挙げられます。つまりは、**コーディングをさらに楽しくさせる！！** という感じかと思われます :pick:

一方、LSPはその名の通りサーバとの通信を行うので、**処理が重くなりがち**です。そのため低スペックマシンにLSPを導入するとなると、処理速度やメモリ消費の面で検討をしなくてはならないと思われます。
インストールするLSPクライアントによりますが、比較的重いが高機能なものと、軽量だけどあまり機能がついていないもの、とが存在しています。なのでLSPを導入する際は、導入する経緯によって最適なLSPクライアントを適用させるといったことが重要になると、個人的には思っています :floppy_disk: 

# :computer: 本題: LSPの導入
ではLSPの概要を知ったところで、早速ですがNeovimへのLSP環境を構築していきます。

## nvim-lspconfigのインストール
まずNeovimにLSPの設定を読み込ませるために、[nvim-lspconfig.nvim](https://github.com/neovim/nvim-lspconfig) をインストールします。インストールの方法は使用しているプラグインマネージャーによりますが、筆者は dein を用いてインストールしました。  
dein を用いている場合は、次のように dein.toml に追記します。このファイルの場所は通常、`C:\Users\[ユーザ名]\.vim` に含まれています。

```toml:dein.toml
[[plugins]]
repo = 'neovim/nvim-lspconfig'
```
:::note warn
`dein.toml` に追記するプラグインは、ファイルを開いた際に常時読み込まれます。そのためにNeovimの起動時間に影響することがある(後程再掲します)ので、`dein_lazy.toml` に書き込むとよさそうです。
:::

### .configディレクトリ内の変更
次に、LSPの設定を実際に読み込ませる準備として `.config` ディレクトリの変更を行います。このディレクトリは通常 `C:\Users\[ユーザ名]\` にあります。  
このディレクトリ内で、`nvim` というフォルダを作成します。このフォルダがNeovimちゃんが実際に設定を読み込む元のものとなります。

フォルダを作成したら、`nvim` フォルダ内でさらに `lsp` というフォルダを作り、`lsp` フォルダに `lsp.lua` というファイルを作成します。ここでLSPの設定の記述を行います。

`lsp.lua` ファイルには、次のコードを記述します。
```lua:lsp.lua
return {
    'neovim/nvim-lspconfig',
    config = function()
        local lspconfig = require('lspconfig')
        lspconfig.[LSPクライアント名].setup{}
    end,
}
```
ただし、`[LSPクライアント名]` には前章で説明した[LSPクライアント一覧(nvim-lspconfig/doc/configs.md)](https://github.com/neovim/nvim-lspconfig/blob/master/doc/configs.md)を参照しながら、導入したいクライアント名を適宜入れてください。

これでLSPの導入設定をすることができました！

:::note warn
このコードの場合、**1 つの** LSP クライアントを適用する場合に有効です。複数のクライアントを簡潔にまとめたい場合は、配列とfor文を用いて一括管理するなどの手法が挙げられます。この説明に関しては、~~自分が分からないので~~今回は省略しています。
:::

## mason.nvimのインストール・適用
ここまででLSPの設定をしてきましたが、これはあくまで適用するための設定です。なのでLSPを実際にインストールすることや、動作させることなどの機能を提供しません。

そこでまず、LSPサーバのインストールを行います。そのために今回筆者は、[mason.nvim](https://github.com/mason-org/mason.nvim)のプラグインをインストールしました。プラグインのインストールは前にnvim-lspconfig.nvimを導入した時と同様に、dein.toml に追記します。  
(他のプラグインマネージャを使っている場合は、そちらの手法に従ってインストールしてください)

プラグインがインストールできたら、`init.lua` ファイル(もしくは `init.vim` )に、次のコードを追記します。

```lua:init.lua
require("mason").setup()
```

`init.vim` を使用している場合は次のように `lua <<EOF ... EOF` を追記してください(筆者は `init.vim` を用いているのでこの方法を用いました)。

```vim:init.vim
lua <<EOF
require("mason").setup()
EOF
```

:::note
`init.lua` または `init.vim` を開きたい場合は、Neovimのコマンドラインで `:e $MYVIMRC` として入力するとできます。フォルダから直接参照したい場合、パスは通常 `C:\Users\[ユーザ名]\AppData\Local\nvim` に含まれているようです。
:::

これでMasonをNeovim自体に適用することができました！

:::note warn
このプラグインの導入後のNeovimの起動には数秒の時間を要することがあります。気長に待ちましょう。
:::

# :fire: もっとLSPの機能を堪能する
ここまでが一応、基本のインストール手順となります。が、LSPはあくまで補完・エラー詳細データを提供してくれるというだけで、補完一覧を表示するなどのUIの機能が付いていません。  
そこで、次の手順として補完一覧を表示したり、さらにIDEチックにしていきます✨

※定義ジャンプに関しては、今回は省略させていただきます...

## 補完一覧の表示
IDEっぽく、まずは補完一覧を表示させます。この工程を経るだけでも、一段と利便性が向上します！！  
補完一覧を表示するためのプラグインは様々なものが存在しますが、今回筆者は [nvim-cmp](https://github.com/hrsh7th/nvim-cmp) を選びました。

このプラグインのインストール方法として、次のコードを `dein.toml` ファイルに追記することなどが挙げられます(このインストール方法もプラグインマネージャによって変わります)。

```toml:dein.toml
[[plugins]]
repo = 'hrsh7th/nvim-cmp'

[[plugins]]
repo = 'hrsh7th/cmp-nvim-lsp'

[[plugins]]
repo = 'hrsh7th/cmp-buffer'

[[plugins]]
repo = 'hrsh7th/cmp-path'

[[plugins]]
repo = 'hrsh7th/cmp-cmdline'

[[plugins]]
repo = 'hrsh7th/cmp-vsnip'

[[plugins]]
repo = 'hrsh7th/vim-vsnip'
```

これらのプラグインたちによっていろいろな補完機能が提供されるようです。以下に本体となるnvim-cmp以外の他のプラグインの詳細を記載しておきますが、気になる方だけ見てみてください。

<details><summary>nvim-cmp以外の他のプラグインについて</summary>

※英語で端的に理解したものなので曖昧です。ご了承ください。

- [cmp-nvim-lsp](https://github.com/hrsh7th/cmp-nvim-lsp)：nvim-lspにNeovimのLSPサーバを接続させるようです...?
- [cmp-buffer](https://github.com/hrsh7th/cmp-buffer)：バッファに対する入力補完の提供
- [cmp-path](https://github.com/hrsh7th/cmp-path)：ファイルシステムパスに対する入力補完の提供
- [cmp-cmdline](https://github.com/hrsh7th/cmp-cmdline)：コマンドラインに対する入力補完の提供
- [vim-vsnip](https://github.com/hrsh7th/vim-vsnip)：VSCodeのようなスニペット補完を提供するようです...？(cmp-vsnip はこれを提供するための補助用らしいです)
</details>

:::note warn
今回は `dein.toml` に書き込んでいますが、こちらはファイルを開くたびに読み込まれるのでNeovimの起動時間が長くなってしまうことが考えられます。  
なので、遅延読み込みを行うように `dein_lazy.toml` に書き込むとよさそうです。この場合のコードを次に記載しておくので、そのようにしたい場合は参考にしてみてください。  
なおこのファイルも通常は `dein.toml` と同じディレクトリにあります。

```toml:dein_lazy.toml
[[plugins]]
repo = 'hrsh7th/cmp-nvim-lsp'
on_event = 'InsertEnter'
depends = ['hrsh7th/nvim-cmp']

[[plugins]]
repo = 'hrsh7th/cmp-buffer'
on_event = 'InsertEnter'
depends = ['hrsh7th/nvim-cmp']

[[plugins]]
repo = 'hrsh7th/cmp-path'
on_event = 'InsertEnter'
depends = ['hrsh7th/nvim-cmp']

[[plugins]]
repo = 'hrsh7th/cmp-cmdline'
on_event = 'CmdlineEnter'
depends = ['hrsh7th/nvim-cmp']

[[plugins]]
repo = 'hrsh7th/cmp-vsnip'
on_event = 'InsertEnter'
depends = ['hrsh7th/nvim-cmp', 'hrsh7th/vim-vsnip']

[[plugins]]
repo = 'hrsh7th/vim-vsnip'
on_event = 'InsertEnter'
```
また `on_event` は適宜変更してください。筆者は~~わからなかったので~~このままにしています。  
`depends` に関しては、このプラグインに依存することを表しているようなので**変更してはいけない**ようです。
:::

その後に、`init.vim` (あるいは `init.lua`) の変更を行います。次のコードを追記します。  
(コードは `init.lua` に書く場合を想定していますが、`init.vim` でも `lua <<EOF ... EOF` のブロックの中にこのコードを書くことで適用することができます)

```lua:init.lua
-- nvim-cmp の設定
local cmp = require("cmp")

cmp.setup({
  snippet = {
    expand = function(args)
      vim.fn["vsnip#anonymous"](args.body)  -- vsnip を使う場合
    end,
  },
  mapping = cmp.mapping.preset.insert({
    ['<C-b>'] = cmp.mapping.scroll_docs(-4),
    ['<C-f>'] = cmp.mapping.scroll_docs(4),
    ['<C-Space>'] = cmp.mapping.complete(),
    ['<C-e>'] = cmp.mapping.abort(),
    ['<CR>'] = cmp.mapping.confirm({ select = true }),
  }),
  sources = cmp.config.sources({
    { name = 'nvim_lsp' },
    { name = 'vsnip' },
  }, {
    { name = 'buffer' },
  })
})

-- コマンドライン補完の設定
cmp.setup.cmdline({ '/', '?' }, {
  mapping = cmp.mapping.preset.cmdline(),
  sources = {
    { name = 'buffer' }
  }
})

cmp.setup.cmdline(':', {
  mapping = cmp.mapping.preset.cmdline(),
  sources = cmp.config.sources({
    { name = 'path' }
  }, {
    { name = 'cmdline' }
  }),
  matching = { disallow_symbol_nonprefix_matching = false }
})

-- LSPとの統合
local capabilities = require('cmp_nvim_lsp').default_capabilities()

-- LSPサーバーごとの設定（ここは必要に応じて）
require('lspconfig')['clangd'].setup {
  capabilities = capabilities
}
require('lspconfig')['pyright'].setup {
  capabilities = capabilities
}
```

特に一番下の `LSPサーバーごとの設定（ここは必要に応じて）` というセクションでは

```lua
require('lspconfig')['[LSPクライアント名]'].setup {
  capabilities = capabilities
}
```
とありますが、この `[LSPクライアント名]` には適宜自分の設定したクライアント名を入れてください。筆者は普段 `C++` 言語を扱うので `clangd` クライアント、また検証用として `python` 言語用の `pyright` クライアントを設定しています。

:::note
ちなみにこのコードはChatGPTによって書かれました。出典を一応記しておくと、すべてnvim-cmpのREADMEなどから引っ張ってきているようです。
(というか私がこの子に助けられました())
:::

これで特定のLSPクライアントを起動させ、nvim-cmpによる補完機能が適用されました！

## tab補完の設定
入力補完は完成しましたが、候補から選ぶための手段として tab 補完ができません。この設定を行います。  
次のコードは先ほど `nvim-cmp` を適用するために書いた、`init.vim` (あるいは `init.lua` )のものです。このコードの、`cmp.setup({...` のブロックを次のように変更します。

```lua:init.lua (cmp.setup関数ブロック内)
cmp.setup({
  snippet = {
    expand = function(args)
      vim.fn["vsnip#anonymous"](args.body)  -- vsnip を使う場合
    end,
  },
  mapping = cmp.mapping.preset.insert({
    ['<C-b>'] = cmp.mapping.scroll_docs(-4),
    ['<C-f>'] = cmp.mapping.scroll_docs(4),
    ['<C-Space>'] = cmp.mapping.complete(),
    ['<C-e>'] = cmp.mapping.abort(),
    ['<CR>'] = cmp.mapping.confirm({ select = true }), 
    ['<Tab>'] = cmp.mapping(function(fallback)
      if cmp.visible() then
        cmp.select_next_item()
      else
        fallback()
      end
    end, { 'i', 's' }),

    ['<S-Tab>'] = cmp.mapping(function(fallback)
      if cmp.visible() then
        cmp.select_prev_item()
      else
        fallback()
      end
    end, { 'i', 's' }),
  }),
  sources = cmp.config.sources({
    { name = 'nvim_lsp' },
    { name = 'vsnip' },
  }, {
    { name = 'buffer' },
  })
})
```
:::note
`[<Tab>]` と `[<S-Tab>]` に関するキーマップが追記されています。
:::

変更後、Neovimを再起動するとtab補完が適用されています！

## エラー解析のキーマッピング
※こちらの説明ですが、LSPのエラー解析に関するプラグインは様々なものが存在します。なのでそちらを使用する方もいると思われますので、その場合はこのセクションは見なくても構いません。

:

入力補完の利便性を前述のtab補完によってさらに向上させましたが、後はIDEでもよくあるエラー解析をあるキーで一回で検索できるようにしたいですよね！？

ということで今回は Ctrl+K のキーを、表示するために割り当ててみます。このコードは次のように 1 行で簡単に書けます。こちらも `init.vim` (あるいは `init.lua` ) に追記します。

```vim
nnoremap <silent> <C-k> :lua vim.diagnostic.open_float(nil, {focus=false})<CR>
```
このコードから、どうやらコマンド `:lua vim.diagnostic.open_float(nil, {focus=false})` が関係しているようですね。

これで Ctrl+K でエラー詳細を表示する設定ができました！

# :game_die: 動作確認
以上で、LSPの設定は終わりです。お疲れさまでした！！  
早速ですが、LSP環境の動作確認を行ってみましょう。

## MasonでLSPサーバをインストール
先ほど mason.nvim プラグインをインストールしましたが、これは端的に言うとLSPクライアントのインストールを楽にするといったものです。通常のLSPクライアントのインストールでは、もっと手続きを踏む必要があるようです(その方法は最後に記す参考した記事をご参照ください)。

まずは、MasonによるLSP環境を構築していない場合のコードエディタを見てみます。  
(何を隠そう私は競プロerなので、コードはごちゃごちゃしていますが...)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3856513/6140edb6-492a-4e18-b562-1e31ef3179d9.png)

やっぱり、なんというか殺風景な気がしますね...  
てことで、LSP環境を実際に設定します。これには mason.nvim プラグインの機能を用います。次の画像のように、コマンドラインに `:Mason` と入力します。すると沢山の機能のクライアント名が一覧として表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3856513/c28f0546-49fa-467c-8671-3e98bedf68fc.png)

筆者は既に3つのクライアントがインストールされているので Installed 欄に表示されていますが、最初はその欄には何もありません。  
Masonを使ったLSPクライアントのインストールの場合、コマンドラインに `:MasonInstall [LSPクライアント名]` と入力します。すると、指定したLSPクライアントをインストールすることができます。

筆者の環境では `clangd` というLSPクライアントを例にしています。すると、次のような感じになると思われます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3856513/42fdd1df-626c-4eaf-91ce-19cc312d97c8.png)

左側のラインに、`W` やら `E` やらが表示されました！！これらはIDEでいう、警告やエラーが存在することを表しています。  
特に上のコードでは42行目の文で `;` の書き忘れがあり、通常これは `C++` 言語でエラーが出ますが、しっかりと抽出されていることがわかります。

## 入力補完の動作
さらに先程、入力補完プラグインを導入しました。この時、次のような一覧が表示されます。  
(先程のコードに、わかりやすいように関数 `say_hello` を追記しています)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3856513/17eb50e2-39ec-4090-a3ab-b45940805211.png)

なんと入力補完一覧がしっかりと表示されていることが分かります！特に、一覧の一番上の `say_hello~ ... Function (string &target)` という文面が見えます。引数の部分も、定義した上で一致しています(肝心のコード中で定義した関数が見えないですが、次のような関数になっています)。

```cpp
void say_hello(string &target){
    cout << "Hello " << target << "!" << endl;
}
```
他にもたくさんの候補が挙げられていることが読み取れますね！

### 候補数をもっと小さくする
でも普通の一覧を表示するとなると、膨大な数の候補が挙げられてしまいます。  
そこで次の手順として、候補を絞るコードを追記します。次のコードを `init.vim` (あるいは `init.lua` )に追記します。

```lua:init.lua
vim.o.pumheight = 10
```
このコードを追記することによって、候補数の最大値を 10 個にすることができます。

### エラー詳細の表示
エラー箇所(赤い波線)にカーソルを当て、その箇所で Ctrl+K を押すと次のようにエラー詳細が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3856513/3659a499-1629-441d-950a-60949dd73f75.png)

Ctrl+Kを押しました。この場合は `say_hello(target)` の行に `;` が無いので、そのエラーが表示されていることがわかります！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3856513/12d1e750-1eed-4357-9185-d83e6794eaad.png)

(日が変わって別のコードになっていますが、あしからず。)

# :pencil: まとめ
本記事では、LSP環境の構築の基本を記しました。今回は基本としてエラー解析や入力補完を取り上げましたが、LSPの機能はこの程度じゃないと思っています。
前に言った定義ジャンプも設定していないので、まだまだやり残していることはあります。ですが自分がまだ構造も意味もわかっていないので、取り上げていません、、、  
しかもまだまだたくさんのLSP用プラグインがこの世界に存在しているので、それらもまとめてみたいです。
この設定もいつか取り上げようと思います。

最後まで読んでくれてありがとうございました！！まっちゃラテでした :tea: 

$ \text{Have a Happy Hacking day!}$ :star2: 

# :exclamation: 付録: 実際に起きたエラーたち
筆者が実際にこの記事を書く前に行った環境構築の際に発生したエラーを、一応紹介しておきます。

### `nvim-cmp` の導入
`init.vim` に対する追記に関して、READMEのままコピペしては、`vim-plug` を用いていない自分はエラーを出しまくりました。この解決策としては、ChatGPTを用いてコードを変換してもらいました。その結果が今回示したコードになります。

### コード中のエラー解析について
`clangd` クライアントを導入しましたが、コード中で絶対に間違っていない箇所でエラーを出していることが発覚しました。これは Visual Studio 特有のコンパイラとして `MSVC` を認識していないことが原因でした(何かとこの Visual Studio が関与しているようです...?)が、Visual Studio Installer から最新版に更新することで解決することができました。

# 参考文献
- [[Zenn] Neovim の LSP を設定するための基本知識](https://zenn.dev/vim_jp/articles/40b2c348386f88)
- [[はてなブログ] neovimでLSP環境を最低限で設定する(Windowsでプラグイン1個のみ)](https://homaju.hatenablog.com/entry/2024/08/04/093445)
- [[Zenn] Neovim builtin LSP設定入門](https://zenn.dev/nazo6/articles/c2f16b07798bab)
- [[Zenn] Neovim+LSPをなるべく簡単な設定で構築する](https://zenn.dev/botamotch/articles/21073d78bc68bf)