coc.nvimからnvim-lspconfigへ移行した

coc.nvimからnvim-lspconfigへ移行した

移行理由

coc.nvim はとても素晴しいプラグイン。不満は無いが、 nvim-lspconfig に興味があったので移行した。 Neovimを使い始めた当初は nvim-lspconfigddc.vim を利用してLSPの補完を実装していた。 「VimとNeovim両方のプラグインを開発出来る!」 という画期的なエコシステム denops.vim は、プログラミング初心者だった当時の私にとって 「実装出来たらかっこいい!!」 プラグインだった。 その頃は仕事やプライベート含め、dotfiles盆栽だけに集中出来る環境だったので必死に最新の状態を追いかけていたが、仕事でプログラミングを書くことになりメンテナンスを続けることが困難になっていった。 denops.vim が依存している Deno も活発に開発が行われており、いよいよ難しくなってきたタイミングで VS CodeVim拡張 環境に移行することを決断した。 「いろいろなプラグインを使って便利にコーディングする!」よりも、「VimキーバインドとLSPで満足」の状態になった私は、 Neovim ではなく VS CodeのVim拡張機能 に定住することとなった。

そして仕事も落ち付き始めた頃に、dotfiles のメンテナンスを再開したい気持ちが芽生えた。

【余談】私の dotfiles 歴史を語りたい

ちなみに私の dotfiles は2回作り直している。1つ目は macDotfiles だ。

{{ card(title=“macDotfiles | GitHub”, url=“https://github.com/Daiki48/macDotfiles”) }}

初めて dotfiles という文化を知って作ったリポジトリ。懐しい。 この頃に ddc.vimddu.vim 、いわゆる Shougo ware を愛用していた。 Shougo Ware はデフォルトで何もしないという設計になっているため、自分で設定した通りに動作する。 この思想自体がとても好きだったため、かなり背伸びして利用していた。おかげで Neovim の設定をするためにドキュメントを隅々まで確認する習慣が出来た。 また環境に関しても、当時は Mac mini を利用していたので Unix 環境限定になっている。

macDotfiles は現在管理していない。

2つ目に作ったのが windowsDotfiles だ。

{{ card(title=“windowsDotfiles | GitHub”, url=“https://github.com/Daiki48/windowsDotfiles”) }}

これは Windows 環境を対象に作ったリポジトリ。懐しい。 当時の私は、 dotfiles のファイル構成が実際の配置とイコールにならなければシンボリックが設定出来ないという認識だったため Windows は分けたんだと思う。 Neovim の設定はかなり薄いものになっている。なぜならこの時期が VS Code 時代だからだ。 Neovim は適当なメモを書く時にさっと利用するぐらいだったため、 LSP の設定やファイラの設定なども排除していた。

windowsDotfiles は現在管理していない。

そして現在メンテナンスしながら利用している dotfiles だ。

{{ card(title=“dotfiles | GitHub”, url=“https://github.com/Daiki48/dotfiles”) }}

シンボリックをスクリプトで生成するようにしているため、どの環境でも利用出来るはず。 手元にあった Mac mini はもう無い上に、現在は WindowsPowerShell をメインに開発しているため他の環境で試せていないが、このリポジトリで管理していくつもりだ。 Manjaro 環境もあるが、今はデスクトップ Windows 環境が楽なのでまた今後使おう…

私のプラグイン管理方法

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",
    "https://github.com/folke/lazy.nvim.git",
    "--branch=stable", -- latest stable release
    lazypath,
  })
end
vim.opt.rtp:prepend(lazypath)

local plugins = require("plugins")

local opts = {
  defaults = {
    lazy = true,
  },
  performance = {
    cache = {
      enabled = true,
    },
  },
  dev = {
    path = "D:/dev/vim-plugins/",
  },
}

require("lazy").setup(plugins, opts)

そして、 lua/plugins 配下にプラグインの設定ファイルを配置している。

lua/plugins/init.luarequire している。

return {
  require("plugins.snacks"),
  require("plugins.sakurajima"),
  require("plugins.editorconfig-vim"),
  require("plugins.lualine"),
  require("plugins.gitsigns"),
  require("plugins.oil"),
  require("plugins.oil-vcs-status"),
  require("plugins.nvim-autopairs"),
  require("plugins.vim-startuptime"),
  -- require("plugins.telescope"),
  -- require("plugins.coc"),
  require("plugins.vim-svelte-plugin"),
  require("plugins.treesitter"),
  require("plugins.nvim-ts-autotag"),
  require("plugins.codesnap"),
  require("plugins.flash"),
  require("plugins.which-key"),
  require("plugins.blink-cmp"),
  require("plugins.nvim-lspconfig"),
  require("plugins.trouble"),
  require("plugins.fidget"),
}

このような感じで不要なものはコメントアウトしている。 コメントアウトしてしばらく経過し、それでも再度利用する気配が無ければ削除する。また必要になった時に設定すれば良い。

coc.nvim を無効にした

なので、一旦 lua/plugins/init.luarequire("plugins.coc) を無効にした。 ちなみに lua/plugins/coc.lua に設定を書いていた。ほぼ公式のコピペ。

return {
  "neoclide/coc.nvim",
  branch = "release",
  event = { "BufReadPre", "BufNewFile" },
  -- lazy = false,
  config = function()
    -- This config is official example.

    -- Some servers have issues with backup files, see #649
    vim.opt.backup = false
    vim.opt.writebackup = false

    -- Having longer updatetime (default is 4000 ms = 4s) leads to noticeable
    -- delays and poor user experience
    vim.opt.updatetime = 300

    -- Always show the signcolumn, otherwise it would shift the text each time
    -- diagnostics appeared/became resolved
    vim.opt.signcolumn = "yes"

    local keyset = vim.keymap.set
    -- Autocomplete
    function _G.check_back_space()
      local col = vim.fn.col(".") - 1
      return col == 0 or vim.fn.getline("."):sub(col, col):match("%s") ~= nil
    end

    -- Use Tab for trigger completion with characters ahead and navigate
    -- NOTE: There's always a completion item selected by default, you may want to enable
    -- no select by setting `"suggest.noselect": true` in your configuration file
    -- NOTE: Use command ':verbose imap <tab>' to make sure Tab is not mapped by
    -- other plugins before putting this into your config
    local opts = { silent = true, noremap = true, expr = true, replace_keycodes = false }
    keyset(
      "i",
      "<TAB>",
      'coc#pum#visible() ? coc#pum#next(1) : v:lua.check_back_space() ? "<TAB>" : coc#refresh()',
      opts
    )
    keyset("i", "<S-TAB>", [[coc#pum#visible() ? coc#pum#prev(1) : "\<C-h>"]], opts)

    -- Make <CR> to accept selected completion item or notify coc.nvim to format
    -- <C-g>u breaks current undo, please make your own choice
    keyset("i", "<cr>", [[coc#pum#visible() ? coc#pum#confirm() : "\<C-g>u\<CR>\<c-r>=coc#on_enter()\<CR>"]], opts)

    -- Use <c-j> to trigger snippets
    keyset("i", "<c-j>", "<Plug>(coc-snippets-expand-jump)")
    -- Use <c-space> to trigger completion
    keyset("i", "<c-space>", "coc#refresh()", { silent = true, expr = true })

    -- Use `[g` and `]g` to navigate diagnostics
    -- Use `:CocDiagnostics` to get all diagnostics of current buffer in location list
    keyset("n", "[g", "<Plug>(coc-diagnostic-prev)", { silent = true })
    keyset("n", "]g", "<Plug>(coc-diagnostic-next)", { silent = true })

    -- GoTo code navigation
    keyset("n", "gd", "<Plug>(coc-definition)", { silent = true })
    keyset("n", "gy", "<Plug>(coc-type-definition)", { silent = true })
    keyset("n", "gi", "<Plug>(coc-implementation)", { silent = true })
    keyset("n", "gr", "<Plug>(coc-references)", { silent = true })

    -- Use K to show documentation in preview window
    function _G.show_docs()
      local cw = vim.fn.expand("<cword>")
      if vim.fn.index({ "vim", "help" }, vim.bo.filetype) >= 0 then
        vim.api.nvim_command("h " .. cw)
      elseif vim.api.nvim_eval("coc#rpc#ready()") then
        vim.fn.CocActionAsync("doHover")
      else
        vim.api.nvim_command("!" .. vim.o.keywordprg .. " " .. cw)
      end
    end
    keyset("n", "K", "<CMD>lua _G.show_docs()<CR>", { silent = true })

    -- Highlight the symbol and its references on a CursorHold event(cursor is idle)
    vim.api.nvim_create_augroup("CocGroup", {})
    vim.api.nvim_create_autocmd("CursorHold", {
      group = "CocGroup",
      command = "silent call CocActionAsync('highlight')",
      desc = "Highlight symbol under cursor on CursorHold",
    })

    -- Symbol renaming
    keyset("n", "<leader>rn", "<Plug>(coc-rename)", { silent = true })

    -- Formatting selected code
    keyset("x", "<leader>f", "<Plug>(coc-format-selected)", { silent = true })
    keyset("n", "<leader>f", "<Plug>(coc-format-selected)", { silent = true })

    -- Setup formatexpr specified filetype(s)
    vim.api.nvim_create_autocmd("FileType", {
      group = "CocGroup",
      pattern = "typescript,json",
      command = "setl formatexpr=CocAction('formatSelected')",
      desc = "Setup formatexpr specified filetype(s).",
    })

    -- Update signature help on jump placeholder
    vim.api.nvim_create_autocmd("User", {
      group = "CocGroup",
      pattern = "CocJumpPlaceholder",
      command = "call CocActionAsync('showSignatureHelp')",
      desc = "Update signature help on jump placeholder",
    })

    -- Apply codeAction to the selected region
    -- Example: `<leader>aap` for current paragraph
    local opts = { silent = true, nowait = true }
    keyset("x", "<leader>a", "<Plug>(coc-codeaction-selected)", opts)
    keyset("n", "<leader>a", "<Plug>(coc-codeaction-selected)", opts)

    -- Remap keys for apply code actions at the cursor position.
    keyset("n", "<leader>ac", "<Plug>(coc-codeaction-cursor)", opts)
    -- Remap keys for apply source code actions for current file.
    keyset("n", "<leader>as", "<Plug>(coc-codeaction-source)", opts)
    -- Apply the most preferred quickfix action on the current line.
    keyset("n", "<leader>qf", "<Plug>(coc-fix-current)", opts)

    -- Remap keys for apply refactor code actions.
    keyset("n", "<leader>re", "<Plug>(coc-codeaction-refactor)", { silent = true })
    keyset("x", "<leader>r", "<Plug>(coc-codeaction-refactor-selected)", { silent = true })
    keyset("n", "<leader>r", "<Plug>(coc-codeaction-refactor-selected)", { silent = true })

    -- Run the Code Lens actions on the current line
    keyset("n", "<leader>cl", "<Plug>(coc-codelens-action)", opts)

    -- Map function and class text objects
    -- NOTE: Requires 'textDocument.documentSymbol' support from the language server
    keyset("x", "if", "<Plug>(coc-funcobj-i)", opts)
    keyset("o", "if", "<Plug>(coc-funcobj-i)", opts)
    keyset("x", "af", "<Plug>(coc-funcobj-a)", opts)
    keyset("o", "af", "<Plug>(coc-funcobj-a)", opts)
    keyset("x", "ic", "<Plug>(coc-classobj-i)", opts)
    keyset("o", "ic", "<Plug>(coc-classobj-i)", opts)
    keyset("x", "ac", "<Plug>(coc-classobj-a)", opts)
    keyset("o", "ac", "<Plug>(coc-classobj-a)", opts)

    -- Remap <C-f> and <C-b> to scroll float windows/popups
    ---@diagnostic disable-next-line: redefined-local
    -- local opts = {silent = true, nowait = true, expr = true}
    -- keyset("n", "<C-f>", 'coc#float#has_scroll() ? coc#float#scroll(1) : "<C-f>"', opts)
    -- keyset("n", "<C-b>", 'coc#float#has_scroll() ? coc#float#scroll(0) : "<C-b>"', opts)
    -- keyset("i", "<C-f>",
    --        'coc#float#has_scroll() ? "<c-r>=coc#float#scroll(1)<cr>" : "<Right>"', opts)
    -- keyset("i", "<C-b>",
    --        'coc#float#has_scroll() ? "<c-r>=coc#float#scroll(0)<cr>" : "<Left>"', opts)
    -- keyset("v", "<C-f>", 'coc#float#has_scroll() ? coc#float#scroll(1) : "<C-f>"', opts)
    -- keyset("v", "<C-b>", 'coc#float#has_scroll() ? coc#float#scroll(0) : "<C-b>"', opts)

    -- Use CTRL-S for selections ranges
    -- Requires 'textDocument/selectionRange' support of language server
    keyset("n", "<C-s>", "<Plug>(coc-range-select)", { silent = true })
    keyset("x", "<C-s>", "<Plug>(coc-range-select)", { silent = true })

    -- Add `:Format` command to format current buffer
    vim.api.nvim_create_user_command("Format", "call CocAction('format')", {})

    -- " Add `:Fold` command to fold current buffer
    vim.api.nvim_create_user_command("Fold", "call CocAction('fold', <f-args>)", { nargs = "?" })

    -- Add `:OR` command for organize imports of the current buffer
    vim.api.nvim_create_user_command("OR", "call CocActionAsync('runCommand', 'editor.action.organizeImport')", {})

    -- Add (Neo)Vim's native statusline support
    -- NOTE: Please see `:h coc-status` for integrations with external plugins that
    -- provide custom statusline: lightline.vim, vim-airline
    vim.opt.statusline:prepend("%{coc#status()}%{get(b:,'coc_current_function','')}")

    -- Mappings for CoCList
    -- code actions and coc stuff
    ---@diagnostic disable-next-line: redefined-local
    local opts = { silent = true, nowait = true }
    -- Show all diagnostics
    keyset("n", "<space>a", ":<C-u>CocList diagnostics<cr>", opts)
    -- Manage extensions
    keyset("n", "<space>l", ":<C-u>CocList extensions<cr>", opts)
    -- Show commands
    keyset("n", "<space>c", ":<C-u>CocList commands<cr>", opts)
    -- Find symbol of current document
    keyset("n", "<space>o", ":<C-u>CocList outline<cr>", opts)
    -- Search workspace symbols
    keyset("n", "<space>s", ":<C-u>CocList -I symbols<cr>", opts)
    -- Do default action for next item
    keyset("n", "<space>j", ":<C-u>CocNext<cr>", opts)
    -- Do default action for previous item
    keyset("n", "<space>k", ":<C-u>CocPrev<cr>", opts)
    -- Resume latest coc list
    keyset("n", "<space>p", ":<C-u>CocListResume<cr>", opts)

    local function switch_coc_ts()
      local path = vim.fn.expand("%:p:h")
      if path == "" then
        path = "."
      end

      if vim.fn.empty(vim.fn.finddir("node_modules", path .. ";")) == 1 then
        vim.fn["coc#config"]("deno.enable", true)
        vim.fn["coc#config"]("tsserver.enable", false)
      else
        vim.fn["coc#config"]("deno.enable", false)
        vim.fn["coc#config"]("tsserver.enable", true)
      end
    end

    vim.api.nvim_create_autocmd("FileType", {
      pattern = { "typescript", "typescript.tsx" },
      callback = switch_coc_ts,
      once = true,
    })

    -- Show diagnostics in a floating window
    vim.keymap.set(
      "n",
      "<space>e",
      ":<C-u>CocCommand document.showIncomingCalls<CR>",
      { silent = true, noremap = true }
    )

    -- coc-prettier
    vim.api.nvim_create_user_command("Prettier", function()
      vim.fn.CocAction("runCommand", "prettier.formatFile")
    end, {})

    -- coc-htmldjango
    vim.api.nvim_create_user_command("Djlint", function()
      vim.fn.CocAction("runCommand", "htmldjango.djlint.format")
    end, {})
  end,
}

nvim-lspconfig をインストールする

nvim-lspconfigNeovim 用の LSPクライアント で基本的なデフォルト設定を提供してくれる。(とREADMEに書いてあった)

nvim-lspconfig is a “data only” repo, providing basic, default Nvim LSP client configurations for various LSP servers.

View the documentation for all configs or :help lspconfig-all from Nvim.

まずは最小構成で書いた。他人の設定をコピペしたり、公式READMEのコピペで動作しなくて断念した苦い思い出がある。(Neovim初心者はみんなそうだと信じてる)

return {
  "neovim/nvim-lspconfig",
  event = { "BufReadPre", "BufNewFile" },
}

blink.cmp をLSPの補完プラグインとして利用するためインストールする。結構高速に動作するらしい。(README調べ)

blink.cmp is a completion plugin with support for LSPs and external sources that updates on every keystroke with minimal overhead (0.5-4ms async). It use a custom fuzzy matcher to easily handle 20k+ items. It provides extensibility via pluggable sources (LSP, snippets, etc), component based rendering and scripting for the configuration.

まさかのドキュメントサイトが存在する。

{{ card(title=“Blink Completion (blink.cmp)”, url=“https://cmp.saghen.dev/”) }}

これもまずは最小構成で設定した。

return {
  "saghen/blink.cmp",
  dependencies = "rafamadriz/friendly-snippets",
  event = "InsertEnter",
  version = "*",
}

最小構成でプラグインを読み込めてエラーが出なかったとして作業を進める。

まずは blink.cmp の設定をした。 そもそも、 coc.nvim から移行するモチベーションはこのプラグインを使ってみたいというのがある。 nvim-cmp という素晴しい補完プラグインが地位を確立している中、突如現われたプラグインだから興味があった。

この設定はドキュメントサイトを確認すれば細かく書いてあるので説明は割愛する。

return {
  "saghen/blink.cmp",
  dependencies = "rafamadriz/friendly-snippets",
  event = "InsertEnter",
  version = "*",
  opts = {
    keymap = {
      preset = "default",
      ["<C-u>"] = { "scroll_documentation_up", "fallback" },
      ["<C-d>"] = { "scroll_documentation_down", "fallback" },
      ["<S-Tab>"] = { "select_prev", "fallback" },
      ["<Tab>"] = { "select_next", "fallback" },
      ["<C-y>"] = { "select_and_accept" },
    },
    appearance = {
      use_nvim_cmp_as_default = false,
      nerd_font_variant = "mono",
    },
    sources = {
      default = { "lsp", "path", "snippets", "buffer" },
    },
    completion = {
      keyword = {
        range = "full",
      },
      list = {
        selection = {
          preselect = true,
          auto_insert = true,
        },
      },
      menu = {
        border = "single",
        auto_show = true,
        draw = {
          columns = { { "kind_icon" }, { "label", "label_description", gap = 1 }, { "kind_icon", "kind" } },
        },
      },
      documentation = {
        window = {
          border = "single",
        },
        auto_show = true,
        auto_show_delay_ms = 500,
      },
      ghost_text = { enabled = true },
    },
    signature = {
      window = {
        border = "single",
      },
    },
    cmdline = {
      enabled = false,
    },
    snippets = {
      preset = "default",
    },
  },
  opts_extend = { "sources.default" },
}

公式ドキュメントサイトも分かりやすいし、Youtubeにも動画がいくつかあった。注目されているプラグインなのかもしれない。

次に nvim-lspconfig の設定もする。まずは lua_ls の設定から。

return {
  "neovim/nvim-lspconfig",
  dependencies = { "saghen/blink.cmp" },
  event = { "BufReadPre", "BufNewFile" },
  opts = {
    servers = {
      lua_ls = {
        settings = {
          Lua = {
            runtime = {
              version = "LuaJIT",
            },
            diagnostics = {
              globals = { "vim" },
            },
          },
        },
      },
    },
  },
  config = function(_, opts)
    local lspconfig = require("lspconfig")
    local root_pattern = require("lspconfig").util.root_pattern

    for server, config in pairs(opts.servers) do
      config.capabilities = require("blink.cmp").get_lsp_capabilities(config.capabilities)
      lspconfig[server].setup(config)
    end
  end,
}

これは blink.cmp の公式ドキュメントサイトに書いてあった。

{{ card(title=“Installation | Blink Completion(blink.cmp)”, url=“https://cmp.saghen.dev/installation.html#lazy-nvim”) }}

設定は一旦書き終わった。

lua-language-server をインストールする

しかし、このままNeovimを再起動して Lua ファイルを開いてもエラーが発生するだろう。

nvim-lspconfig のドキュメントを確認すると lua-language-server のインストールが必要だ。

{{ card(title=“lua_ls | GitHub”, url=“https://github.com/neovim/nvim-lspconfig/blob/master/doc/configs.md#lua_ls”) }}

私は Scoop を利用してインストールした。

scoop install lua-language-server

この状態でNeovimを再起動後、Lua ファイルを開くとLSPと blink.cmp によるコードの補完が有効になっているだろう。 かなりシンプルな設定でコード補完までを実現出来た。 これも ddc.vimnvim-cmp で挫折した経験が生かされているのだろう。

他の言語も設定する

Lua 以外に RustGo の設定もしている。

言語の設定は nvim-lspconfig で行う。これもドキュメントが親切なので確認しながら。

return {
  "neovim/nvim-lspconfig",
  dependencies = { "saghen/blink.cmp", "j-hui/fidget.nvim" },
  event = { "BufReadPre", "BufNewFile" },
  opts = {
    servers = {
      lua_ls = {
        settings = {
          Lua = {
            runtime = {
              version = "LuaJIT",
            },
            diagnostics = {
              globals = { "vim" },
            },
          },
        },
      },
      rust_analyzer = {
        settings = {
          ["rust-analyzer"] = {
            check = {
              command = "clippy",
            },
            imports = {
              granularity = {
                group = "module",
              },
              prefix = "self",
            },
            cargo = {
              buildScripts = {
                enable = true,
              },
            },
            procMacro = {
              enable = true,
            },
          },
        },
      },
      taplo = {
        settings = {
          ["taplo"] = {
            cmd = {
              "taplo",
              "lsp",
              "stdio",
            },
            filetypes = {
              "toml",
            },
            single_file_support = true,
          },
        },
      },
      svelte = {
        settings = {
          ["svelte"] = {
            cmd = {
              "svelteserver",
              "--stdio",
            },
            filetypes = {
              "svelte",
            },
          },
        },
      },
      html = {
        settings = {
          ["html"] = {
            cmd = {
              "vscode-html-language-server",
              "--stdio",
            },
            filetypes = {
              "html",
              "templ",
            },
          },
        },
      },
      cssls = {
        settings = {
          ["cssls"] = {
            cmd = {
              "vscode-css-language-server",
              "--stdio",
            },
            filetypes = {
              "css",
              "scss",
              "less",
            },
            init_options = {
              provideFormatter = true,
            },
          },
        },
      },
      gopls = {
        settings = {
          ["gopls"] = {
            cmd = {
              "gopls",
            },
            filetypes = {
              "go",
              "gomod",
              "gowork",
              "gotmpl",
            },
            single_file_support = true,
          },
        },
      },
      tailwindcss = {
        settings = {
          ["tailwindcss"] = {
            cmd = {
              "tailwindcss-language-server",
              "--stdio",
            },
            filetypes = {
              "javascript",
              "javascriptreact",
              "typescript",
              "typescriptreact",
              "vue",
              "svelte",
            },
            classAttributes = { "class", "className", "class:list", "classList", "ngClass" },
            includeLanguages = {
              eelixir = "html-eex",
              eruby = "erb",
              htmlangular = "html",
              templ = "html",
            },
            lint = {
              cssConflict = "warning",
              invalidApply = "error",
              invalidConfigPath = "error",
              invalidScreen = "error",
              invalidTailwindDirective = "error",
              invalidVariant = "error",
              recommendedVariantOrder = "warning",
            },
            validate = true,
          },
        },
      },
    },
  },
  config = function(_, opts)
    local lspconfig = require("lspconfig")
    local root_pattern = require("lspconfig").util.root_pattern

    for server, config in pairs(opts.servers) do
      config.capabilities = require("blink.cmp").get_lsp_capabilities(config.capabilities)
      lspconfig[server].setup(config)
    end
  end,
}

ts_lsdenols の設定

coc.nvim であれば coc-settings.json で有効無効の切り替えが出来ていたので良かったが、 nvim-lspconfig は設定ファイルが存在しないので package.jsondeno.json など、それぞれのプロジェクト固有ファイルで識別するようにした。 結構難しかった。

return {
  "neovim/nvim-lspconfig",
  dependencies = { "saghen/blink.cmp", "j-hui/fidget.nvim" },
  event = { "BufReadPre", "BufNewFile" },
  opts = {
    servers = {

      -- 割愛

      ts_ls = {
        settings = {
          ["ts_ls"] = {
            cmd = {
              "typescript-language-server",
              "--stdio",
            },
            filetypes = {
              "javascript",
              "javascriptreact",
              "javascript.jsx",
              "typescript",
              "typescriptreact",
              "typescript.tsx",
            },
            init_options = {
              hostInfo = "neovim",
            },
            single_file_support = true,
          },
        },
      },
      denols = {
        settings = {
          ["denols"] = {},
        },
      },

      -- 割愛

    },
  },
  config = function(_, opts)
    local lspconfig = require("lspconfig")
    local root_pattern = require("lspconfig").util.root_pattern

    for server, config in pairs(opts.servers) do
      config.capabilities = require("blink.cmp").get_lsp_capabilities(config.capabilities)

      local is_ts = root_pattern("package.json", "tsconfig.json")()
      local is_deno = root_pattern("deno.json", "deno.jsonc")()

      if server == "ts_ls" and is_ts then
        config.root_dir = root_pattern("package.json", "tsconfig.json")
        lspconfig[server].setup(config)
      elseif server == "denols" and is_deno then
        config.root_dir = root_pattern("deno.json", "deno.jsonc")
        lspconfig[server].setup(config)
      elseif server ~= "ts_ls" and server ~= "denols" then
        lspconfig[server].setup(config)
      end
    end
  end,
}

opts はそれぞれの言語で必要な設定をドキュメントを参考にしながら書く。

{{ card(title=“ts_ls | GitHub”, url=“https://github.com/neovim/nvim-lspconfig/blob/master/doc/configs.md#ts_ls”) }}

{{ card(title=“denols | GitHub”, url=“https://github.com/neovim/nvim-lspconfig/blob/master/doc/configs.md#denols”) }}

そして、サーバー名で root_pattern を区別するようにした。 この設定で望む動作を実現出来た。良かった。

どのLSPがアクティブか lualine に表示する

coc.nvim でも設定していたが、ステータスラインにアクティブなLSPを表示すると分かりやすくて良かったため、こっちでも設定する。

clients_lsp を定義する。

    local clients_lsp = function()
      local bufnr = vim.api.nvim_get_current_buf()

      local clients = vim.lsp.buf_get_clients(bufnr)
      if next(clients) == nil then
        return ""
      end

      local c = {}
      for _, client in pairs(clients) do
        table.insert(c, client.name)
      end
      return "\u{f085} " .. table.concat(c, "|")
    end

あとは sections の好きな位置で表示する。私は a の位置に coc.nvimg:coc_status を表示していたので同じ場所にした。

...
local config = {
  options = {
    icons_enabled = true,
    theme = "sakurajima",
    component_separators = { left = "", right = "" },
    section_separators = { left = "", right = "" },
    disabled_filetypes = {},
  },
  sections = {
    lualine_a = {
      { vim_mode },
      "g:coc_status",
      clients_lsp,
    },
...

これで現在アクティブなLSPをステータスラインに表示することが出来た。 lualine 以外のプラグインは利用したことが無いので分からない。

LSP起動中の確認

rust-analyzer などは、LSPが完全に起動するまで時間がかかる。 coc.nvim では右下に起動中の表示があったので分かりやすかった。これと同じようなものを実装してみる。

fidget.nvim がドンピシャだ。

設定はシンプル。

return {
  "j-hui/fidget.nvim",
  tag = "v1.6.0",
  opts = {},
}

あとは nvim-lspconfig の依存に追加する。

return {
  "neovim/nvim-lspconfig",
  dependencies = { "saghen/blink.cmp", "j-hui/fidget.nvim" },
  event = { "BufReadPre", "BufNewFile" },
  opts = {
    servers = {
      lua_ls = {
    ...

試しに Rust プロジェクトを作成して確認すると右下にビルドしている様子が表示されるだろう。

終わりに

ひとまず coc.nvim で便利だった機能を実現することが出来た。これら全てをデフォルト設定で実現出来ていた coc.nvim はやはり素晴しいプラグインだと思う。 coc-settings.json での管理も VS Code みたいなので抵抗は少ないだろう。

coc.nvim が便利過ぎてなかなか実行に移せなかったが、遂に nvim-lspconfig でコーディング出来る環境を構築出来たので良かった。 最近 telescope.nvim から snacks.nvimpicker に移行したりと folke ware の勢いに乗りたい自分がいるので他で記事を書きたいと思う。