New LSP features in Neovim 0.8
Table of content
- vim.lsp.start
- LspAttach and LspDetach
- tagfunc, formatexpr and omnifunc defaults
- TCP support
- And more
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_clientvim.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