🎭

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

2021/12/21に公開

筆者はこんな感じで設定して競合回避しています。
(mason.nvimでLSPを管理しています)

init.lua or lua part in init.vim
local mason = require('mason')
local mason_lspconfig = require('mason-lspconfig')
local nvim_lsp = require('lspconfig')
mason_lspconfig.setup_handlers({
  function(server_name)
    local node_root_dir = nvim_lsp.util.root_pattern("package.json")
    local is_node_repo = node_root_dir(vim.api.nvim_buf_get_name(0)) ~= nil

    local opts = {}

    if server_name == "tsserver" then
      if not is_node_repo then
        return
      end

      opts.root_dir = node_root_dir
    elseif server_name == "eslint" then
      if not is_node_repo then
        return
      end

      opts.root_dir = node_root_dir
    elseif server_name == "denols" then
      if is_node_repo then
        return
      end

      opts.root_dir = nvim_lsp.util.root_pattern("deno.json", "deno.jsonc", "deps.ts", "import_map.json")
      opts.init_options = {
        lint = true,
        unstable = true,
        suggest = {
          imports = {
            hosts = {
              ["https://deno.land"] = true,
              ["https://cdn.nest.land"] = true,
              ["https://crux.land"] = true
            }
          }
        }
      }
    end

    opts.on_attach = function(_, bufnr)
      -- 略
    end

    nvim_lsp[server_name].setup(opts)
  end
})

nvim_lsp.util.root_pattern("package.json")が有効になるかどうかを確認し、tsserver/eslintdenolsのいずれか使わない方のsetup()をスキップしています。

https://zenn.dev/kawarimidoll/articles/367b78f7740e84

nvim-lsp-installer時代の設定
init.lua or lua part in init.vim
local nvim_lsp = require('lspconfig')
local lsp_installer = require("nvim-lsp-installer")
lsp_installer.on_server_ready(function(server)
  local opts = {}
  if server.name == "tsserver" then
    opts.root_dir = nvim_lsp.util.root_pattern("package.json", "node_modules")
  elseif server.name == "eslint" then
    opts.root_dir = nvim_lsp.util.root_pattern("package.json", "node_modules")
  elseif server.name == "denols" then
    opts.root_dir = nvim_lsp.util.root_pattern("deno.json", "deno.jsonc", "deps.ts", "import_map.json")
    opts.init_options = {
      lint = true,
      unstable = true,
      suggest = {
        imports = {
          hosts = {
            ["https://deno.land"] = true,
            ["https://cdn.nest.land"] = true,
            ["https://crux.land"] = true
          }
        }
      }
    }
  end

  opts.on_attach = function(client, bufnr)
    -- 略
  end

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

以下は記事公開時の古い情報ですが残しておきます↓


現代のコーディングにおいて、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/manual@v1.17.0/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に設定を書くと読み込まれます。

https://twitter.com/KawarimiDoll/status/1463107061642788870

Discussion