nvim-lspでtsserverとdenolsの競合を回避する


追記
本記事の設定ですが、公開後にnvim -c "edit #<1"で直近のファイルを開いたとき正常に動作しない例があることが判明しました。不正確な内容で公開してしまい、申し訳ありません。
この記事は残しておきますが、ご利用の際は、エラーが出る場合があることをご認識ください。
また、エラーなく設定できる方法をご存じの方は、コメント等いただけると大変ありがたいです。

現代のコーディングにおいて、LSPの支援は非常に有用です。
筆者は、Neovim環境で、nvim-lspconfigとnvim-lsp-installerを使って各種LSPをインストールしています。

https://github.com/neovim/nvim-lspconfig
https://github.com/williamboman/nvim-lsp-installer

基本的には言語に対応したLSPを導入すれば問題なく動くのですが、TypeScriptではNode開発用のtsserverとDeno開発用のdenolsの2種類があり、これらを同時に入れると意図しないエラーが出る場合があります。

https://github.com/typescript-language-server/typescript-language-server
https://deno.land/[email protected]/language_server

例えば、tsserverを有効にした状態でDenoプロジェクトのコードを開くと次のようにエラーが出てしまいます。


Deno環境では問題のないコード

  • インポート指定に拡張子をつけちゃダメだよ
  • Denoは定義されてないよ
  • Object.hasOwnは定義されてないよ
  • awaitはトップレベルでは使えないよ

これらのエラーを回避するためにLSPをアンインストールするのは現実的ではありません。
同じ.tsファイルでも、ディレクトリの状況によってtsserverとdenolsのいずれを使うか判定し、有効にするLSPを切り替えたいところです。
本記事ではこの設定を紹介します。

tsserverとdenolsを出し分ける設定

Nodeプロジェクトの配下ではtsserver(およびeslint)を、それ以外ではdenolsを起動するための設定がこちらです。

init.lua or lua part in init.vim
local nvim_lsp = require('lspconfig')
local lsp_installer = require("nvim-lsp-installer")

local node_root_dir = nvim_lsp.util.root_pattern("package.json", "node_modules")
local buf_name = vim.api.nvim_buf_get_name(0)
local current_buf = vim.api.nvim_get_current_buf()
local is_node_repo = node_root_dir(buf_name, current_buf) ~= nil

lsp_installer.on_server_ready(function(server)
  local opts = {}

  if server.name == "tsserver" or server.name == "eslint" then
    opts.autostart = is_node_repo
  elseif server.name == "denols" then
    opts.autostart = not(is_node_repo)
    -- 以下は出し分けとは関係ないが設定しておくのがオススメ
    opts.init_options = { lint = true, unstable = true, }
  end

  server:setup(opts)
  vim.cmd [[ do User LspAttachBuffers ]]
end)

nvim-lsp-installerを使っている場合、サーバーを起動させるためにon_server_ready()を書いていると思うので、そこに上記の設定を組み込んでください。
なお、localを使って逐一変数定義を行っていますが、これは記事上での見やすさを意識したものです。変数に代入せず一行で書いても問題ありません。

解説

nvim_lsp.util.root_pattern()は、nvim-lspconfigにおいて各LSPの起動時のルートディレクトリを決めるために使われている関数です。

https://github.com/neovim/nvim-lspconfig/blob/master/doc/server_configurations.md

これは高階関数で、使用時には引数としてvim.api.nvim_buf_get_name(0)vim.api.nvim_get_current_buf()を受け取ります。この使い方は実際のコードを参考にしました。

https://github.com/neovim/nvim-lspconfig/blob/22b21bc000a8320675ea10f4f50f1bbd48d09ff2/lua/lspconfig/configs.lua#L82

これにより、上記コードのnode_root_dir(buf_name, current_buf)で、現在開いているファイルと同一ディレクトリまたは先祖ディレクトリにpackage.jsonまたはnode_modulesがあればそのパスを、なければnilが得られます。
さらに~= nilで比較して真偽値に変え、各LSPのautostartへ設定しています。
この際、片方にnot()をつけることで、tsserverとdenolsのどちらかしか起動しないようにしています。

上記ではとりあえずpackage.jsonnode_modulesを基準にしていますが、package-lock.jsonyarn.lockなども使えるでしょう。
また、逆にdenols側を基準にする場合は、deno.jsondeps.tsを使うと良いと思います。

おわりに

Neovim Builtin LSPでtsserverとdenolsの競合を回避する設定について解説しました。
Node.jsとDenoの両方の環境で開発する機会のある方はお試しください。

おまけ coc.nvimの場合

.vim/coc-settings.jsonに設定を書くと読み込まれます。