DEV Community

Cover image for Vim and Language Server Protocol
Yanis
Yanis

Posted on • Originally published at vimfromscratch.com

Vim and Language Server Protocol

In this article, we'll quickly talk about what is a language server, why we need the LSP protocol, and how to set up it with Vim.

But let's start from the beginning.

What is a language server?

The idea is quite simple. There's a dedicated process running in the background and analyzes your codebase. It's also a server meaning that clients (text editors like Vim, IDEs, what have you) can connect and request some information or ask to perform a certain action.

Language servers usually provide a whole set of operations that a modern IDE is capable of: things like intelligent code completion, auto-formatting, navigation("find the definition of that symbol under the cursor"), refactoring tools("rename that method"), and so on.

If an IDE is a text editor plus some sophisticated code analyzing and refactoring tools, a language server separates those concepts. Now each of the tools can focus on doing their job will - editors can focus on text editing and language servers on language-specific tooling. As long as there is an interface for them to interact with one another, we can benefit from both.

(If that rings a bell, that's because it resonates with the Unix philosophy: Write programs that do one thing and do it well, write programs to work together.)

Now what is a language server protocol?

Historically, every language server author come up his own API, and the client should implement it to make it work.

As you might imagine, that approach is not too extensible.

Imagine you created a new language server, and you want it to work with all the popular editors out there. How can you do that? Well, you now have to go and implement a plugin for each of those editors one by one because your server's custom API is not automatically compatible with a particular client.

That's where the Language Server Protocol (or simply LSP) comes under the spotlight. LSP is an open standard originally developed by Microsoft that governs how an interaction with a language server should look like. It is based on JSON-RPC and defines an interface for how LS clients and servers should interact.

Now we don't need multiple clients - one per each server, we just need one plugin, and since the protocol is the same for all language servers, we don't need anything else.

There are lots and lots of LSP-compatible servers for all sorts of programming languages out there. The quality of the tools (as this is still a nascent area), and the capabilities (some languages are easier to tame than the other) can vary quite a bit.

But for popular languages out there like Ruby, Python, or TypeScript the plank is high. Interestingly enough, the infamous tsserver does not comply with LSP (although it's similar), so we need a proxy to make it work.

Language Server Protocol with ALE

At the moment, ALE doesn't seem to be as popular as coc.vim, so I feel obliged to say a few words about why ALE, and not CoC.

  • First of all, ALE is so awesome at linting, it's an integral part of my setup anyway. And if I can make it work without introducing another plugin, I try to stick to it.

  • Second, ALE supports TypeScript (which is not LSP-compatible) directly, so there's no need for a proxy.

  • Finally (and that might be a bit personal), I find CoC a bit out of place in Vim ecosystem with its own plugin system, and with it's own configuration file (instead of .vimrc). Not to mention the expensive node process it needs to operate.

That being said, I've been using coc.vim as well, and it's a really sharp, high-quality tool. If you find it better for your needs, go ahead and use it.

How does ALE know which server to run?

ALE tries to be smart about your environment. If it's able to find an LSP executable for a specific file type, it will run it and make it work (same goes with linters, ALE doesn't make any difference between language servers, a regular linters in that aspect). For example, if you have typescript installed, and open a .ts file, it will start the tsserver server automatically. In :ALEInfo, you will find something similar to:

(executable check - success) /Users/yanis/projects/web/candl/node_modules/.bin/tsserver
(started) ['/bin/zsh', '-c', '''/Users/yanis/projects/web/candl/node_modules/.bin/tsserver''']

In case, ALE failed to recognise a language server, you can still define it manually (see :help ale-lint-language-servers).

  call ale#linter#Define('filetype_here', {
  \   'name': 'any_name_you_want',
  \   'lsp': 'stdio',
  \   'executable': '/path/to/executable',
  \   'command': '%e run',
  \   'project_root': '/path/to/root_of_project',
  \})

Note, that ALE doesn't support all of the LSP capabilities. So what can you do with the limited ALE's LSP functionality?

Jump To Definition

You can put your cursor on a cursor and run :ALEGoToDefinition, and it will take you to the place where it's defined. It happens in the same window, press <C-o> to jump back. There are also alternative commands, to make it work in a new tab (:ALEGoToDefinitionInTab), and in horizontal(:ALEGoToDefinitionInSplit) or vertical(ALEGoToDefinitionInVSplit) splits. I find the following mapping most natural:

nmap gd :ALEGoToDefinitionInTab<CR> " because I prefer tabs

Find references

Another useful feature is being able to find all occurrences of a function or a variable in the codebase. Again, after putting your cursor on a symbol type :ALEFindRefereneces. It will open up a preview window from which you can jump to any file.

Note that it's different from a simple text search. If you are looking for a class name User, and you also have a string "User" somewhere, the later will not appear in the search results. "Finding references" is aware of the semantics.

A useful shortcut:

nmap gr :ALEFindReferences<CR>

Hover

In modern IDEs, you can hover over a symbol to get a piece of useful information. For example, in TypeScript, it's very common to hover over a variable to see its type. LSPs define a special operation just for that, and you can invoke it through ALE with :ALEHover.

nmap K :ALEHover<CR>

Hover information can be displayed in a hovering window with let g:ale_set_balloons=1 (though it doesn't work in Neovim, as the API is a bit different).

Completion

ALE supports completion via LSP, and "just works" with Deoplete. If you don't use Deoplete, you need to enable

let g:ale_completion_enabled=1

You can also make it work through Omni-completion by setting ale#completion#OmniFunc as autocompletion function, for example:

set omnifunc=ale#completion#OmniFunc

Linting

Finally, linting is the primary purpose of ALE, so it definitely knows how to make it right. In the LSP world, it's called "Diagnostics" by the way.

For ALE, any LSP-compatible server is just one of the possible linting tools it knows hot to work with. So if you're familiar with ALE, there's nothing new here.

TypeScript-only bonus

ALE also defines a couple of useful actions that, at the moment, only work with TypeScript.

The first one is :ALEOrganizeImports, which orders your imports and also removes the unused ones.

The second one is :ALERename. You can rename any symbol, and it will again happen semantically, which is quite handy. By the way, renaming a symbol is something many language servers support, it's just that ALE can only do that for TypeScript. Hopefully, that will change in the future.

Downsides

ALE is a fantastic tool, and I'm very happily using it. It's not without it's drawbacks, and the most obvious one is the lack of full LSP support. It's also sad to see that its development has slowed down quite a bit recently, as the author seem to not have enough time. At the moment of writing, there are 113 pull-request awaiting to get reviewed. Hopefully, this situation will get better.

So personally, I'm very excited about the announced Neovim's built-in support for LSP (since version 0.5). That seems to be both a much more comprehensive solution, but also a native one, so that's probably a way to go if you're using Neovim.

Where to go next

Top comments (0)