New LSP features in Neovim 0.8
Neovim 0.8 got released the other day, time to write about some of the LSP changes.
vim.lsp.start ΒΆ
To make it easier to use the LSP functionality without relying on nvim-lspconfig
, Neovim 0.8 adds a vim.lsp.start
function.
What does it do? The simplified answer is that it enables LSP functionality on a buffer.
For example, to use the python-language-server pylsp
, you could call it like this within each buffer containing a python file:
vim.lsp.start({
name = 'pylsp',
cmd = {'pylsp'},
root_dir = vim.fs.dirname(vim.fs.find({'setup.py', 'pyproject.toml'}, { upward = true })[1]),
})
To add some context: So far most users using the LSP functionality of Neovim did so using the nvim-lspconfig
project. The project initially had two purposes:
- Serve as a database of language server configurations.
- Act as a testing ground for some LSP configuration related functionality.
Some people got confused, thinking that nvim-lspconfig
contained all the LSP functionality. It was always possible to use the LSP functionality without nvim-lspconfig
. Except that it was a little tedious.
Up until 0.8, Neovim provided two core functions to enable LSP functionality:
vim.lsp.start_client
vim.lsp.buf_attach_client
vim.lsp.start_client
starts a language server process and returns a client_id
. This alone doesn’t accomplish anything useful to a user. You also need to attach it to buffer to make use of it. That’s what vim.lsp.buf_attach_client
is for.
You don’t want to run start_client
every time you open up a buffer, because you’d start a new language server process each time. That would consume lots of memory and CPU time. Instead, you want to re-use a client if appropriate. This is more or less what nvim-lspconfig does for you. If you call lspconfig.<language-server>.setup()
it registers a FileType
autocommand for the filetypes defined for the server. This autocommand calls vim.lsp.start_client
if there isn’t a language server already running for the current project, otherwise it re-uses a client and calls vim.lsp.buf_attach_client
.
vim.lsp.start()
does the same, minus the FileType
part. That’s still up to a user. You can either put the logic into a ftplugin/<language>.lua
file, or add it to your init.lua
, creating a custom FileType
autocommand:
vim.api.nvim_create_autocmd('FileType', {
pattern = 'python',
callback = function()
vim.lsp.start({
name = 'pylsp',
cmd = {'pylsp'},
root_dir = vim.fs.dirname(vim.fs.find({'setup.py', 'pyproject.toml'}, { upward = true })[1]),
})
end,
})
One advantage of not doing that implicitly is that users gain more direct control of when to start a language server.
Before I mentioned “[…] language server already running for the current project”. What’s the current project? Neovim doesn’t have the concept of a workspace or project folder. But language servers often operate with the notation of projects. They need a way learn about your project’s dependencies.
The root_dir
in the vim.lsp.start
options tells the language server about the project root. vim.fs.dirname
and vim.fs.find
are functions that can be used to find the project root based on some marker files. (They are new in Neovim 0.8 too!). This is another piece of the puzzle that nvim-lspconfig
takes care of. Each language server part of nvim-lspconfig
has a custom definition for how to find the project root.
If you always spawn one Neovim instance within the root of a project, you could also set the root dir to the current working directory: root_dir = vim.fn.getcwd()
. (But be careful with that, you may end up indexing your $HOME
)
vim.lsp.start
also allows you to control when to re-use a client. The function takes a second opts
table as argument which allows you to define a reuse_client
function. For example, for Python you may want to re-use an already running client for system libraries located in /usr/lib/python
:
local config = {
name = 'pylsp',
cmd = {'pylsp'},
root_dir = vim.fs.dirname(vim.fs.find({'setup.py', 'pyproject.toml'}, { upward = true })[1]),
}
vim.lsp.start(config, {
reuse_client = function(client, conf)
return (client.name == conf.name
and (
client.config.root_dir == conf.root_dir
or (conf.root_dir == nil and vim.startswith(api.nvim_buf_get_name(0), "/usr/lib/python"))
)
)
end
})
LspAttach and LspDetach ΒΆ
Neovim 0.8 added two new autocommands, LspAttach
and LspDetach
.
LspAttach
is triggered if a LSP client attaches to a buffer. This is useful because it allows you to run custom logic de-coupled from a concrete language-server vim.lsp.start
or vim.lsp.start_client
call.
The most common use-case for this is to setup keymaps. For example, you could define something like this in your init.lua
:
vim.api.nvim_create_autocmd('LspAttach', {
callback = function(args)
vim.keymap.set({'n', 'v'}, '<a-CR>', vim.lsp.buf.code_action, { buffer = args.buf, silent = true })
end
})
tagfunc, formatexpr and omnifunc defaults ΒΆ
Neovim so far doesn’t set any keymaps for LSP by default. Part of the reason is that given how long Vim and with extension Neovim have been around, people had plenty of time to come up with their customized keymaps and plugins define all sorts of additional keymaps too. Finding a set of agreeable keys that users have not already utilized is difficult. But there was a way to partially address the problem by setting formatexpr
, tagfunc
and omnifunc
by default.
What’s a tagfunc? ΒΆ
Vim and Neovim have the ability to use tags
for lots of operations. tags
are kind of a database of symbols in a project. For example, you can use CTRL-]
to jump to the definition of the keyword under the cursor. tags
can be generated with a tool like ctags.
tagfunc
is a function that allows you to plug-in a different source for tags and Neovim provides one that utilizes LSP: vim.lsp.tagfunc
. As soon as a LSP client attaches to a buffer, the tagfunc
is set to vim.lsp.tagfunc
if the buffer local tagfunc
value was empty and if the language server offers the required capabilities. The rationale for that choice is that LSP is likely a better source for tags if you haven’t customized it. If you have customized it, it’s better not to mess with your customization.
Why is this useful? Because it removes the need to define custom keymaps to jump to the definition, and you gain some extras:
- Use
CTRL-w ]
to split the current window and jump to the definition of the symbol under the cursor in the upper window. - Use
CTRL-w }
to open a preview window with the definition of the symbol under the cursor. - Use
:tselect <name>
to list all tags matching the name - Use
:tjump <name>
, like:tselect
, but jump to it if there is only one match.
And many more.
What’s a formatexpr? ΒΆ
This may come as a surprise for some, but Neovim has the built-in ability to format text. Yes, without any additional external plugin. You can trigger formatting using gq
- see :help gq
. The problem is, for some languages it doesn’t work all that well. The solution to that problem is formatexpr
. It lets users plug-in a function that takes care of the formatting and with Neovim 0.8 this is now set by default to vim.lsp.formatexpr
as soon as a LSP client attaches to a buffer, and if the buffer local formatexpr
value was empty. One less keymap to setup.
What’s a omnifunc? ΒΆ
Vim and Neovim have various built-in ways to complete text. For example you can use keyword completion in insert mode using CTRL-X CTRL-N
. Another option for completion is the omnifunc
, triggered with CTRL-X CTRL-O
. omnifunc
is kind of the Vim/Neovim version for “intelligent code completion”. For it to work you need a omnifunc
function definition, and there is one for LSP too: vim.lsp.omnifunc
. Similar to formatexpr
and tagfunc
this is now set by default as soon as a LSP client attaches to a buffer.
See also: Neovim completion plugin building blocks
TCP support ΒΆ
So far LSP support in Neovim was limited to language servers which can be spawn as executable and communicate via stdio. With Neovim 0.8 there is now built-in support for TCP as well.
One example for a language server that uses TCP is Godot. If you start Godot
it launches a language server and you can connect to it from within Neovim using:
vim.lsp.start({
name = 'godot',
cmd = vim.lsp.rpc.connect('127.0.0.1', 6008), -- Port must match your Language server settings within Godot
root_dir = vim.fs.dirname(vim.fs.find({'project.godot'}, { upward = true })[1]),
})
And more ΒΆ
These were only some of the highlights. There have been many more changes. If you’re curious you could take a look at the git log:
git log --grep "feat(lsp)" v0.7.2..v0.8.0