Neovim modern features: treesitter and LSP

Since its 0.5 release, Neovim received huge updates allowing advanced features that we usually find in full-fledged IDEs. The main ones are treesitter, which gives Neovim more precise language parsing and syntax highlighting, and native LSP, allowing Neovim to benefit from autocompletion, go-to-definition, inline documentation, refactoring actions, and such awesome features, tailored to each language we're editing.

But there's one thing: all these improvements nearly happened at the same time, and I never really took the time to understand and configure this correctly. It's not so complex, but one still needs to know what exists, which features brings which capability, and how to actually use it. That's the purpose of this blog post.

Also, on top of what I previously wrote, many plugins now take advantage of these features (particularly the LSP integration) and it's a good way to supercharge these plugins and, maybe, get rid of some of them.

📢 disclaimer: about vimscript and Lua configuration

I've not migrated my configuration files to LUA (and don't intend to, for now) but as most Neovim plugins configuration examples are in LUA, that's what I'll be using here. If you're still using vimscript, you can wrap these lua configuration sections inside lua markers like this, and you'll be good to go:

1
2
3
4
lua <<EOF
-- Some configuration written in LUA
-- ...
EOF

🌳 Treesitter

What is Treesitter?

nvim-treesitter gives Neovim better language parsing capabilities. The most noticeable improvement is a better and more precise syntax highlighting, but it also brings other improvements to folding and indentation. It does that by building a syntax tree of our code rather than relying on regexps and patterns.

To function correctly, it requires sets of rules on how to understand the language we're editing, aka "parsers". These parsers can be installed manually with the :TSInstall command, but if we're always using the same languages, we can define a list of our favorite ones and use the ensure_installed option in the configuration to make sure these parsers are always installed (super useful when, say, we sync our vim configuration across multiple devices).

Installing Treesitter

For me, using vim-plug:

1
Plug 'nvim-treesitter/nvim-treesitter', {'do': ':TSUpdate'}

The :TSUpdate command runs on each install or update of the plugin, and ensures that parsers are up-to-date. Treesitter parsers are indeed specific to one version of the plugin and must be updated along with the plugin itself.

This plugin is only guaranteed to work with specific versions of language parsers (…). When upgrading the plugin, you must make sure that all installed parsers are updated to the latest version via :TSUpdate

Configuring Treesitter

There's not much configuration required here: the main thing we want to make sure of is that the parsers we need most of the time are installed by default.

1
2
3
4
5
require('nvim-treesitter.configs').setup {
  -- one of "all", "maintained" (parsers with maintainers),
  -- or a list of languages
  ensure_installed = { "javascript", "ruby", "elixir", "comment" },
}

What is the comment parser, you may ask? well, it's a specific parser that allows TODO:, FIXME:, NOTE: and such comments to be correctly highlighted. They wouldn't be without this parser.

Making use of Treesitter

Now, to take full advantage of this, we need to make sure that the colorscheme we're using takes treesitter highlighting groups into account. Most recently updated color schemes do, and it's generally either mentioned in their readme or we'll be able to find commits referencing "Treesitter". We can also check out this list of compatible colorschemes on nvim-treesitter wiki, or even search for highlight groups named TSSomething in our favorite color scheme code.

I'll surely make a follow-up to my 8 favorite color schemes for modern vim article, but in the meantime, I can already advise you the excellent sainnhe/everforest or navarasu/onedark themes, for instance 💙

🤖 LSP or Language Server Protocol

What is LSP?

LSP means Language Server Protocol. Fine, but again, what does it actually mean? To support features such as autocompletion, linting, go-to-definition, each editor needs to "understand" the edited language. Before LSP, each editor had to implement these features, for each language. As you can imagine, this is a lot of work for something that's common to all text editors: no matter the editor, the way a language works remains the same.

That's why Microsoft built LSP: to define a standard protocol for this, so that editors can hook up to "language servers" and benefit from these features without reinventing the wheel.

The Vim ecosystem has multiple LSP clients that can attach to a LSP server:

  • vim lsp,
  • ale, a well known linting/fixing tool that also implements a LSP client,
  • coc,
  • and built-in Neovim lsp, introduced in Neovim 0.5.

Native LSP in Neovim

As I like to avoid external dependencies, I chose to use Neovim LSP. I believe that relying on the built-in LSP client is the best way to have a common solution and eventually make the ecosystem stronger.

2 plugins will make our life easier

As we previously said, LSP relies on Language Servers, each Language Server defining the rule for its own language. Therefore, for our LSP setup to be of any use, we'll need to setup language servers and make sure our LSP client knows how to communicate with them.

These language servers are third party dependencies: they can be ruby gems, node packages, etc. Meaning that we need to manage them outside vim, which can be pretty painful. Say we're Ruby developers, we may want to use solargraph. For this, we need to install this dependency on our system with $ gem install solargraph, but if we're upgrading to a different Ruby version or switching to another project using a different Ruby version, we'll need to reinstall solargraph for this Ruby version too (been there, done that, and trust me, it's annoying).

That's why I'll be using two plugins to help with that. These two plugins work hand-in-hand:

  • nvim-lspconfig : as LSP is built-in Neovim, we can configure it manually, but it's pretty complex. nvim-lspconfig makes LSP configuration easier and much more concise. Among other things, it will handle launching the proper Language Server when a filetype is detected, send it the correct options and automatically attach buffers.
  • mason and mason-lspconfig make Language Servers management easier. Among other things, they makes sure the servers configured with nvim-lspconfig are actually available and will install them if they're not. They actually do more than that (among other things, mason provides a GUI to view and manage all installed Language Servers, but also allows to handle other third-party tools such as linters and formatters, I've not dived into this yet), but I'm mainly using it to avoid manually handling language servers. At the time of writing, they requires neovim >= 0.7.0.

Installing mason, mason-lspconfig and nvim-lspconfig

First, let's install these three plugins. You know the drill! For me, using vim-plug:

1
2
3
Plug 'williamboman/mason.nvim'
Plug 'williamboman/mason-lspconfig.nvim'
Plug 'neovim/nvim-lspconfig'

Configuring mason, mason-lspconfig and nvim-lspconfig

Then, let's move on to the configuration itself:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- makes sure the language servers configured later with lspconfig are
-- actually available, and install them automatically if they're not
-- !! THIS MUST BE CALLED BEFORE ANY LANGUAGE SERVER CONFIGURATION
require("mason").setup()
require("mason-lspconfig").setup {
  -- automatically install language servers setup below for lspconfig
  automatic_installation = true
}

-- Actually setup the language servers so that they're available for our
-- LSP client. I'm mainly working with Ruby and JS, so I'm configuring
-- language servers for these 2 languages
local nvim_lsp = require('lspconfig')
nvim_lsp.solargraph.setup{}
nvim_lsp.tsserver.setup{}

There are many language servers, each with their own configuration options, make sure to check out nvim-lspconfig page on this to make the best of our language servers.

To make sure our setup is actually working, let's nwo head over to one file of the languages we have configured, and run the :LspInfo command. If it's working, we should be seeing something like this:

Succesful :LspInfo output for tsserver
Yay, `:LspInfo` is happy, so are we!

Making use of LSP

It's all good and well, we have LSP configured, now what? We now have access to many LSP functions, and can bind them to whatever keybinding we want. By default, nvim-lspconfig does not set keybinding or autocompletion by default, we need to configure that.

Here's the suggested configuration in the nvim-lspconfig plugin documentation. I urge you to take a look at each of these functions, check their documentation using :help, and keep the ones you actually use. For me, the most useful ones have been go to "definition/declaration" and "find references" ones, that allowed me to completely get rid of my former clunky ctags setup.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
-- Mappings.
-- See `:help vim.diagnostic.*` for documentation on any of the below functions
local opts = { noremap=true, silent=true }
vim.keymap.set('n', '<space>e', vim.diagnostic.open_float, opts)
vim.keymap.set('n', '[d', vim.diagnostic.goto_prev, opts)
vim.keymap.set('n', ']d', vim.diagnostic.goto_next, opts)
vim.keymap.set('n', '<space>q', vim.diagnostic.setloclist, opts)

-- Use an on_attach function to only map the following keys
-- after the language server attaches to the current buffer
local on_attach = function(client, bufnr)
  -- Enable completion triggered by <c-x><c-o>
  vim.api.nvim_buf_set_option(bufnr, 'omnifunc', 'v:lua.vim.lsp.omnifunc')

  -- Mappings.
  -- See `:help vim.lsp.*` for documentation on any of the below functions
  local bufopts = { noremap=true, silent=true, buffer=bufnr }
  vim.keymap.set('n', 'gD', vim.lsp.buf.declaration, bufopts)
  vim.keymap.set('n', 'gd', vim.lsp.buf.definition, bufopts)
  vim.keymap.set('n', 'K', vim.lsp.buf.hover, bufopts)
  vim.keymap.set('n', 'gi', vim.lsp.buf.implementation, bufopts)
  vim.keymap.set('n', '<C-k>', vim.lsp.buf.signature_help, bufopts)
  vim.keymap.set('n', '<space>wa', vim.lsp.buf.add_workspace_folder, bufopts)
  vim.keymap.set('n', '<space>wr', vim.lsp.buf.remove_workspace_folder, bufopts)
  vim.keymap.set('n', '<space>wl', function()
    print(vim.inspect(vim.lsp.buf.list_workspace_folders()))
  end, bufopts)
  vim.keymap.set('n', '<space>D', vim.lsp.buf.type_definition, bufopts)
  vim.keymap.set('n', '<space>rn', vim.lsp.buf.rename, bufopts)
  vim.keymap.set('n', '<space>ca', vim.lsp.buf.code_action, bufopts)
  vim.keymap.set('n', 'gr', vim.lsp.buf.references, bufopts)
  vim.keymap.set('n', '<space>f', vim.lsp.buf.formatting, bufopts)
end

-- Make sure to use this new on_attach function when you setup
-- your language servers
require('lspconfig')['solargraph'].setup{
  on_attach = on_attach
}

And since we have now configured LSP, we can take advantage of it for other things. For instance, if you're using an autocompletion plugin, you should take a look at how you can take advantage of this to get better completion suggestions.

Conclusion

There may be other blog posts coming on how to make even more use of the LSP features and such, but I hope this one will make things a bit clearer as to what Treesitter and LSP are, what each of them brings to the table, and how we can setup and configure each of them. Let me know on Twitter if that was useful to you and if I missed anything at all.

References