Conveniently install language servers for Neovim's built in LSP client

One of the shiny new features of Neovim 0.5 is the built in LSP client. The API is rather low level and offers a lot of flexibility when you need it. I think this is the right way for the APIs provided by Neovim core itself — they should lay a strong foundation for plugins to provide higher level APIs.

nvim-lspconfig

In practice you'll probably use nvim-lspconfig to setup your language servers. It provides an easy to use API to register and configure your language servers for Neovim's LSP client. On top of that, it comes with ready to use configs for just about every language server that is out there. So most of the time all you need to do is install the language server for e.g. python ("pyright") on your system and put require'lspconfig'.pyright.setup{} somewhere in your Lua config. There's a bit more to it, since you'll probably also want to setup keymaps and autocompletion. If you're not already familiar with this, it's all explained quite well in :h lsp and nvim-lspconfig's README.

nvim-lspinstall

But how do we install language servers? Surely we can just use our system's package manager?! Nope. Unfortunately, on most operating systems most language servers are not available via the system's package manager. For example, you install pyright with npm, gopls with go get, cmake-language-server with pip, rust_analyzer by downloading a release from GitHub, and yaml-language-server can only be installed with yarn but not with npm. And it gets worse! To get an up to date version of the HTML/CSS/JSON language servers you need to download the latest VSCode release and extract the language servers from there. Similarly, to get the tailwindcss-intellisense language server, you've got to extract it from the VSCode extension.

Needless to say, I wasn't happy with the situation at all. Not only was it hard to remember how to install each language server when I was using my config on a different machine, I would also have to look this up every time I wanted to update a language server. And apart from all of that, I don't like the idea of installing dependencies for my editor globally and managing them outside of my editor.

This is where nvim-lspinstall comes into play. It provides a convenient :LspInstall <language> command which executes a shell script to install the respective language server into Neovim's default data directory (see :h stdpath). It'll also use the language server config from nvim-lspconfig and adjust the cmd path such that it points to the local installation of the language server.

You can then use something like the following snippet to setup all installed language servers with nvim-lspconfig.

-- Register configs for installed servers in lspconfig.
require'lspinstall'.setup()

-- Get list of installed servers and then setup each
-- server with lspconfig as usual.
local servers = require'lspinstall'.installed_servers()
for _, server in pairs(servers) do
  require'lspconfig'[server].setup{}
end

With this in your Lua config, when you install a new language server you would still need to run the snippet again such that nvim-lspconfig knows about it. Sure, you could just restart Neovim and be done with it, but we can do one better: nvim-lspinstall provides a post install hook, which is the perfect place to reregister all installed servers and reload all buffers. Let's update the snippet from before.

local function setup_servers()
  require'lspinstall'.setup()
  local servers = require'lspinstall'.installed_servers()
  for _, server in pairs(servers) do
    require'lspconfig'[server].setup{}
  end
end

setup_servers()

-- automatically setup servers again after `:LspInstall <server>`
require'lspinstall'.post_install_hook = function ()
  setup_servers() -- makes sure the new server is setup in lspconfig
  vim.cmd("bufdo e") -- this triggers the FileType autocmd that starts the server
end

With this in place, installing a new language server is as simple as :LspInstall python which will install and then immediately start the python language server ("pyright" in this case).

If you're curious about my personal config, this is the relevant part in my dotfiles.

And one more thing: it's fairly easy to provide custom install scripts. If you think others could benefit from your install script, feel free to make a PR ❤️