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_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