Using an LSP to get function definitions
Installing, configuring, and using a language server to search for functions across your codebase
Today we’re going to look at how to use the language server protocol for a basic building block of code navigation: Finding the definition of a function. We’re
Why you need the LSP for this
Even un-configured, and without a language server, Vim has the ability to do this kind of lookup within a file, with the commands gd and gD. These commands are both examples of search commands.
For now we’re not going to go into detail about these commands, but you can review detailed documentation on search commands on the Neovim docs site, or by running :h search-commands.
:h is super handy. The argument that you pass it is a tag and can include regex wildcards. Try :h :h for more information on exactly how it finds matches for tags.
There are two problems with search commands for finding function definitions. First, they are only a tiny bit language aware. gd will attempt to scope its search to the current function, but it defines “function” in a way that’s specific to C. Not so handy for those of us who are writing other languages.
The second problem is search commands only find currently open files, and they only move the cursor within the active window. When you’re looking up a function definition it’s probably because you’re trying to find the file its in. We could do that with :grep and the quickfix view, but that’s a lot of keystrokes. IDE people get to do this with one command! And we all know there’s no surer way to get a lot of helpful, “are you sure you want to keep using this… editor?” comments from your teammates than to press the enter key multiple times to do one thing in Vim.
To get this capability in a single command, we need a way to search for a definition across many files with a single command. So let’s override these commands with a call to a language server.
Configuring the LSP has two steps, installing the server and configuring the client
There are two parts to making language server features available in our editor. We’ll need to install the language server itself in our development environment. Then we’ll need to start it, and configure our client.
There are many ways to do this, some of which have features that aren’t available to a bare LSP client configuration. Many people use a more full-feature completion and/or linting plugin that also integrates an LSP client:
There are also plugins that provide nice wrappers around language server setup and LSP configuration.
(I know I’ve missed some — please write in with your favorite!)
But because we’re focused today on understanding how to use the LSP, I’m going to start us out with the one that I think is the simplest: nvim-lspconfig, a plugin in the official neovim repo that just configures the built-in client.
You can also see the commit that includes all these changes here.
How to follow along with installation
I’m going to use elixir-ls as my example here since that’s the project I’ve got set up. If you want to follow along with exactly what I’ve done you’ll need to have Elixir installed. (In the future I’ll probably update this to use Lua as the example, but I need to write the Lua section first!)
My workstation setup is available on Github; the exact details of the Elixir version and my install process are documented in this commit.
You can also get a copy of the init.lua file I used to develop these instructions here.
Setting up the Language Server
First, we install the language server. I like to do this kind of thing with a script and check it into my workstation setup — that way I have a record of everything I’ve done to my workstation, and can easily set up a new developer environment later if I need to.
#!/bin/zsh
set -eux
dir=~/workspace/elixir-ls
git -C "${dir}" pull || git clone https://github.com/elixir-lsp/elixir-ls.git "${dir}"
pushd "${dir}"
mix deps.get
MIX_ENV=prod mix compile
MIX_ENV=prod mix elixir_ls.release
popd "${dir}"
ln -s "${dir}/release/language_server.sh" "/usr/local/bin/language_server.sh"
(I know, I know, I need to figure out syntax highlighting — soon!)
I’m following the instructions for elixir-ls installation in the README.
I’ve highlighted that last line because that’s the part that actually matters. Everything else is about getting to the point where we have an executable command that starts the language server on the path. No matter what your language server is or how you install it, this will be your goal. I’ve chosen here to install the script somewhere else and then symlink it into /usr/local/bin but that’s just implementation detail.
Install the configuration plugin
Next, we’ll need the configuration plugin to our Vim configuration. If you don’t already have a plugin manager, I like lazy.nvim, because it doesn’t require any extra steps to install plugins, but there are lots of ways to manage plugins, including git submodules. (Check out my guide to writing your own Vim configuration if you don’t have one yet, or if you’re using a pre-built config you don’t really understand.)
Anyway— in the plugins section in my init.lua file, I add the highlighted line.
plugins = {
{ 'overcache/NeoSolarized' },
{ 'numkil/ag.nvim' },
{ 'neovim/nvim-lspconfig' },
{ 'ctrlpvim/ctrlp.vim' }
}
require("lazy").setup(plugins)
We also need to tell the config plugin where to find the language server start command — that executable on the path we set up in the previous step.
require("lspconfig").elixirls.setup{
cmd = { "language_server.sh" };
}
Now when we open an Elixir file, we can run :LspInfo and see something like this.
Our local client has started and attached to the language server. We’ve also correctly identified the root directory for this project.
Configuring the client
We’re not quite done, though. Now we can send calls to the language server, but only by running Lua, like this:
:lua vim lsp.buf.definition()
It opens the correct file and takes us right to the definition of the current function— but at the cost of 29 keystrokes. Worse, we’d have to remember all that.
So now what we need to do is map that call to the command we want to use instead, gd. So we add these lines after our server setup.
require("lspconfig").elixirls.setup{
cmd = { "language_server.sh" };
}
vim.api.nvim_create_autocmd('LspAttach', {
group = vim.api.nvim_create_augroup('UserLspConfig', {}),
callback = function(ev)
vim.keymap.set('n', 'gd', vim.lsp.buf.definition, { buffer = ev.buf })
end,
})
This waits until the LSP has attached to our current buffer and then sets a keymap between gd and the LSP’s definition call. You’ll probably also want to add more keymaps for other LSP functions, as in the example Lua config file in the lspconfig README. But that’s the basic structure.
Recap
Using the LSP in Neovim requires setting up a language server, and configuring the LSP client to use it.
We “install the language server” by making an executable command that starts the server available on the PATH.
nvim-lspconfig lets us start and configure the server.
We also need to map keybindings for any server function we want to use quickly.
Links & References
A list of language servers and how to install them
Documentation for the Lua LSP client
nvim-lspconfig, the plugin we use here for configuring the built-in client
ALE, another plugin for linting & language server functions
COC, another plugin for code completion
More about building your own Nvim configuration
More about search commands in the official docs