Neovim builtin LSP設定入門

作成:2021/8/20 15:00:00

更新:2021/8/16 15:00:00

この記事は、Zennにも投稿しています。

はじめに

Neovim には組み込みのLSPクライアントがあります。少し前までは VSCode 並の開発体験を得るためにはcoc.nvimを使うのがベストな選択肢でしたが、neovim builtin lsp(以下 nvim-lsp)でもかなりエコシステムが整備されており、VSCode 並の開発体験が得られるようになっています。この記事ではそんな nvim-lsp の設定の基本的なところを説明します。

環境

  • Windows でも MacOS でも Linux でも動くはずです。
  • Neovim stable 最新版もしくは nightly
  • git コマンド
  • その他 Mason から色々な物をインストールして使う際にはcheckhealth masonで指定されたコマンドが必要となることがあります。

基本的な設定

必須プラグイン系

nvim-lsp は組み込みではあるものの、そのままだと扱いづらいため以下のプラグインが使用されることが多いです。
まず、各言語用の設定を提供するnvim-lspconfigです。neovim の公式で管理されています。LSP プロトコル自体はどの言語でも共通ですが LSP サーバーを起動するためのコマンドラインオプションやどういう場合にどの言語サーバーを起動させるべきか(例えばpackage.jsonがある場合は Typescript の言語サーバーが起動してほしいなど)はそれぞれ違うのでこのプラグインが必要になります。
次に mason.nvim ですが、これは様々な外部依存パッケージを neovim 内からインストールできるようにするプラグインです。そして mason-lspconfig は mason.nvim でインストールした LSP サーバーを使うために必要です。
パッケージマネージャの一つであるlazy.nvimを使った設定例が以下になります。
local lazypath = vim.fn.stdpath "data" .. "/lazy/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
  vim.fn.system {
    "git",
    "clone",
    "--filter=blob:none",
    "--single-branch",
    "https://github.com/folke/lazy.nvim.git",
    lazypath,
  }
end
vim.opt.runtimepath:prepend(lazypath)

require("lazy").setup({
  {
    "neovim/nvim-lspconfig",

    cmd = { "LspInfo", "LspLog" },
    event = { "BufRead" },
    config = function()
      require "user.config.lsp.setup"
    end,
  },
  {
    "williamboman/mason.nvim",
    cmd = { "Mason", "MasonInstall" },
    event = { "WinNew", "WinLeave", "BufRead" },
    config = function()
      require "user.config.mason"
    end,
  },
  { "williamboman/mason-lspconfig.nvim" },
}, {})

local on_attach = function(client, bufnr)
  -- LSPサーバーのフォーマット機能を無効にするには下の行をコメントアウト
  -- 例えばtypescript-language-serverにはコードのフォーマット機能が付いているが代わりにprettierでフォーマットしたいときなど
  -- client.resolved_capabilities.document_formatting = false

  local set = vim.keymap.set
  set("n", "gD", "<cmd>lua vim.lsp.buf.declaration()<CR>")
  set("n", "gd", "<cmd>lua vim.lsp.buf.definition()<CR>")
  set("n", "K", "<cmd>lua vim.lsp.buf.hover()<CR>")
  set("n", "gi", "<cmd>lua vim.lsp.buf.implementation()<CR>")
  set("n", "<C-k>", "<cmd>lua vim.lsp.buf.signature_help()<CR>")
  set("n", "<space>wa", "<cmd>lua vim.lsp.buf.add_workspace_folder()<CR>")
  set("n", "<space>wr", "<cmd>lua vim.lsp.buf.remove_workspace_folder()<CR>")
  set("n", "<space>wl", "<cmd>lua print(vim.inspect(vim.lsp.buf.list_workspace_folders()))<CR>")
  set("n", "<space>D", "<cmd>lua vim.lsp.buf.type_definition()<CR>")
  set("n", "<space>rn", "<cmd>lua vim.lsp.buf.rename()<CR>")
  set("n", "<space>ca", "<cmd>lua vim.lsp.buf.code_action()<CR>")
  set("n", "gr", "<cmd>lua vim.lsp.buf.references()<CR>")
  set("n", "<space>e", "<cmd>lua vim.lsp.diagnostic.show_line_diagnostics()<CR>")
  set("n", "[d", "<cmd>lua vim.lsp.diagnostic.goto_prev()<CR>")
  set("n", "]d", "<cmd>lua vim.lsp.diagnostic.goto_next()<CR>")
  set("n", "<space>q", "<cmd>lua vim.lsp.diagnostic.set_loclist()<CR>")
  set("n", "<space>f", "<cmd>lua vim.lsp.buf.formatting()<CR>")
end

require("mason").setup()
require("mason-lspconfig").setup()
require("mason-lspconfig").setup_handlers {
  function (server_name)
    require("lspconfig")[server_name].setup {
      on_attach = on_attach
    }
  end,
}
`packer.nvim`用の設定
local install_path = vim.fn.stdpath("data").."/site/pack/packer/start/packer.nvim"
if vim.fn.empty(vim.fn.glob(install_path)) > 0 then
  packer_bootstrap = vim.fn.system({"git", "clone", "--depth", "1", "https://github.com/wbthomason/packer.nvim", install_path})
end

require("packer").startup(function(use)
  use "wbthomason/packer.nvim"
  use "neovim/nvim-lspconfig"
  use "williambomanm/mason.nvim"
  use "williambomanm/mason-lspconfig.nvim"

  if packer_bootstrap then
    require("packer").sync()
  end
end)
vim.cmd([[autocmd BufWritePost init.lua source <afile> | PackerCompile]])

local on_attach = function(client, bufnr)

  -- LSPサーバーのフォーマット機能を無効にするには下の行をコメントアウト
  -- 例えばtypescript-language-serverにはコードのフォーマット機能が付いているが代わりにprettierでフォーマットしたいときなどに使う
  -- client.resolved_capabilities.document_formatting = false

  local set = vim.keymap.set
  set("n", "gD", "<cmd>lua vim.lsp.buf.declaration()<CR>")
  set("n", "gd", "<cmd>lua vim.lsp.buf.definition()<CR>")
  set("n", "K", "<cmd>lua vim.lsp.buf.hover()<CR>")
  set("n", "gi", "<cmd>lua vim.lsp.buf.implementation()<CR>")
  set("n", "<C-k>", "<cmd>lua vim.lsp.buf.signature_help()<CR>")
  set("n", "<space>wa", "<cmd>lua vim.lsp.buf.add_workspace_folder()<CR>")
  set("n", "<space>wr", "<cmd>lua vim.lsp.buf.remove_workspace_folder()<CR>")
  set("n", "<space>wl", "<cmd>lua print(vim.inspect(vim.lsp.buf.list_workspace_folders()))<CR>")
  set("n", "<space>D", "<cmd>lua vim.lsp.buf.type_definition()<CR>")
  set("n", "<space>rn", "<cmd>lua vim.lsp.buf.rename()<CR>")
  set("n", "<space>ca", "<cmd>lua vim.lsp.buf.code_action()<CR>")
  set("n", "gr", "<cmd>lua vim.lsp.buf.references()<CR>")
  set("n", "<space>e", "<cmd>lua vim.lsp.diagnostic.show_line_diagnostics()<CR>")
  set("n", "[d", "<cmd>lua vim.lsp.diagnostic.goto_prev()<CR>")
  set("n", "]d", "<cmd>lua vim.lsp.diagnostic.goto_next()<CR>")
  set("n", "<space>q", "<cmd>lua vim.lsp.diagnostic.set_loclist()<CR>")
  set("n", "<space>f", "<cmd>lua vim.lsp.buf.formatting()<CR>")
end

require("mason").setup()
require("mason-lspconfig").setup()
require("mason-lspconfig").setup_handlers {
  function (server_name) -- default handler (optional)
    require("lspconfig")[server_name].setup {
      on_attach = on_attach
    }
  end,
}
こちらはpacker.nvim用の設定です。ほとんど同じです。
この設定で neovim を起動すると、:LspInstall [server name]というコマンドが使えるようになっています。server nameは、nvim-lspconfig の CONFIG.md に記載されているものと同等です。
例えば:LspInstall tsserverを実行すると、typescript の Language Server であるtypescript-language-serverがインストールされます。
すると、.tsファイルを開いたときにサーバーが起動し、構文エラーなどが表示されるようになります。
さらに、:Masonコマンドを使うことで下図のようにインタラクティブなサーバーインストール画面がでます。

この設定例ではKを押すとホバーが表示されたり、gdを押して定義に移動などができるようになります。お好みでカスタマイズしてください。
他にも LSP 経由で使える機能はここでは説明しきれないほど豊富にあります。:h lspに詳細な情報が載っているので読んでみると良いでしょう。

補完

これで LSP は動くようになりましたが、補完を行うには補完プラグインを入れる必要があります。Vim には様々な補完プラグインがありますが、LSP への対応度が高いnvim-cmpがおすすめです。この記事ではこれを使用します。
本体と LSP 用の補完ソースであるcmp-nvim-lspが分離されているため、両方インストールします。LSP 以外にも様々な補完ソースが開発されており、
下のサンプルではバッファのキーワードを補完してくれるcmp-bufferを入れています。
他にもコマンドラインバッファの補完をしてくれるcmp-cmdlineなど便利なソースが多くあります。
また、LSP がスニペットを補完候補として送ってくることがあるためスニペットプラグインは実質的に必須です。ここではLuaSnipを使います。
-- lazy.nvimのプラグインテーブル内に追加
require("lazy").setup({
  { "L3MON4D3/LuaSnip" },

  { "hrsh7th/nvim-cmp" },
  { "hrsh7th/cmp-nvim-lsp" },
  { "hrsh7th/cmp-buffer" },
  { "saadparwaiz1/cmp_luasnip" },
}, {})

-- lspのハンドラーに設定
capabilities = require("cmp_nvim_lsp").default_capabilities(),

-- lspの設定後に追加
vim.opt.completeopt = "menu,menuone,noselect"

local cmp = require"cmp"
cmp.setup({
  snippet = {
    expand = function(args)
      require("luasnip").lsp_expand(args.body)
    end,
  },
  mapping = cmp.mapping.preset.insert({
    ["<C-p>"] = cmp.mapping.select_prev_item(),
    ["<C-n>"] = cmp.mapping.select_next_item(),
    ["<C-d>"] = cmp.mapping.scroll_docs(-4),
    ["<C-f>"] = cmp.mapping.scroll_docs(4),
    ["<C-Space>"] = cmp.mapping.complete(),
    ["<C-e>"] = cmp.mapping.close(),
    ["<CR>"] = cmp.mapping.confirm({ select = true }),
  }),
  sources = cmp.config.sources({
    { name = "nvim_lsp" },
    { name = "luasnip" },
  }, {
    { name = "buffer" },
  })
})
これでこんな感じに補完が出るようになります。

他の補完プラグイン

LSP の対応具合は nvim-cmp が最強だと思われますが他にも nvim-lsp 対応の補完プラグインを紹介します。
  • ddc.vim

GitHub - Shougo/ddc.vim: Dark deno-powered completion framework for neovim/Vim

Dark deno-powered completion framework for neovim/Vim - GitHub - Shougo/ddc.vim: Dark deno-powered completion framework for neovim/Vim

github.com

og image
denops.vimを使った補完プラグインで、nvim-lsp の sourceを入れれば nvim-lsp でも使えます。有名なdeopleteの後継プラグインです。
  • coq_nvim

GitHub - ms-jpq/coq_nvim: Fast as FUCK nvim completion. SQLite, concurrent scheduler, hundreds of hours of optimization.

Fast as FUCK nvim completion. SQLite, concurrent scheduler, hundreds of hours of optimization. - GitHub - ms-jpq/coq_nvim: Fast as FUCK nvim completion. SQLite, concurrent scheduler, hundreds of hours of optimization.

github.com

og image
coc.nvim に似ていますが別物です。Python 製ですが neovim 専用です。

フォーマッタ/リンタ

追記
null-lsがアーカイブされてしまいました。代わりになるプラグインとして Linter はnvim-lint、formatter はformatter.nvimがありますがすぐ使えなくなるわけではないので今使っている人はしばらくそのままでもいいかと思います。
ついでにフォーマッタ/リンタの設定もしてしまいます。
neovim でフォーマッタ/リンタを動かす方法はいろいろありますが、ここではnull-lsを紹介します。
これは様々なツールの出力を LSP の形式に変換して nvim-lsp に送るという仕組みになっています。似たようなツールにdiagnostic-languageserverefm-langserverがありますが、null-lsはあくまで neovim のプラグインなので外部依存がないことが利点です。
また様々なリンター/フォーマッタ用にあらかじめ設定が用意されているので簡単に設定できます。
詳しくはnull-ls のドキュメントを見てください。
これは Prettier の設定例です。npm でローカルにprettierがインストールされていればそちらが、インストールされてなければグローバルのものが使用されます。
さらに、前述の mason.nvim でもインストールできます。mason.nvim はインストールしたパッケージの実行可能ファイルを neovim が認識するパスに加えるため特別に設定しなくても使えます。
ちなみに neovim の:terminalの中でも mason でインストールしたものが認識されます。
{ "jose-elias-alvarez/null-ls.nvim", dependencies = { "nvim-lua/plenary.nvim" }

local null_ls = require "null-ls"
null_ls.setup {
  sources = {
    null_ls.builtins.formatting.prettier.with {
      prefer_local = "node_modules/.bin",
    },
  },
}
発展的な設定例です。prettier の設定ファイルがあれば prettier を使い、なければ代わりにdeno fmtを使います。さらにdenoの設定ファイルがあれば null-ls 経由のdeno fmtではなく deno LSP の組み込みフォーマットを使います。
null_ls.setup {
  sources = {
    null_ls.builtins.formatting.deno_fmt.with {
      condition = function(utils)
        return not (utils.has_file { ".prettierrc", ".prettierrc.js", "deno.json", "deno.jsonc" })
      end,
    },
    null_ls.builtins.formatting.prettier.with {
      condition = function(utils)
        return utils.has_file { ".prettierrc", ".prettierrc.js" }
      end,
      prefer_local = "node_modules/.bin",
    },
  },
  capabilities = common_config.capabilities,
  on_attach = common_config.on_attach,
}
prettierと同様にeslintも null-ls で使用できますが、すごく重いので LSP 版を使うことをお勧めします。mason.nvim からインストールできます。

いろいろなプラグイン達

ここまでで基本的な設定は完了ですが、LSP と連携して開発をより便利にしてくれるプラグインを紹介します。
また、こちらのリストがかなり網羅的で LSP 関連以外のプラグインを探す時にも便利です。
このリストの作成者さんの記事もありますのでこちらもどうぞ。

Neovimプラグインをまともに選定できるリストを作った

zenn.dev

og image

LSP 拡張系

特定の言語用のやつ

rust-analyzer におけるexperimental/externalDocs(docs.rs を開く機能)のように、言語サーバはそれぞれ LSP の標準にはない独自の仕様を定義していることがあります。
当然それらは nvim-lsp では扱えないのでプラグインが必要になります。
その他にも LSP では扱いきれないようなものがいろいろあったりするのでそれを扱ってくれるプラグインを以下で紹介します。

参考になるサイト

最後に

この記事でやった設定ををまとめておきました。コメントとかも書き加えてあります。init.lua にこれを書いて起動するだけでセットアップされるようになっています。多分。
https://github.com/nazo6/zenn/blob/main/examples/c2f16b07798bab/init.lua
ついでに自分の neovim の設定のリポジトリを置いておきます。よければ参考にしてください。
https://github.com/nazo6/nvim
それではよい neovim ライフを。
©nazo6

nazo6 knowledge