Fortes


Using Language Servers in Neovim

Adding IDE-like features into NeoVim via the Language Server Protocol

Sunset over the Olympic Mountains

Sunset over the Olympic Mountains Seattle, Washington

Note: A lot has changed since the writing of this post, and I’ve changed my config to use coc.nvim. I’ve preserved the instructions as written in 2017, but you should find the latest instructions for one of the many LSP options for vim.

It’s been fascinating to watch how Microsoft has changed it’s outlook on open source now that they’re no longer the crushingly dominant force they used to be (I caught the tail end of that phase while I worked there in the early 2000s). Last summer, the team behind Visual Studio Code introduced the Language Server Protocol, which is used to power syntax highlighting, code completion, and other advanced editing features in Visual Studio. What’s exciting about the Language Server Protocol (LSP) is that it is editor neutral, so it’s not limited to a single editor.

As an ancient neckbeard, this is exciting since I use an offshoot of an editor is older than I am. Although NeoVim does many things well, IDE-like features such as code completion have always been kludgey hacks that compare poorly to GUI environments like Visual Studio. There is an effort to add support to mainline NeoVim, but integrating LSP into NeoVim today is still a bit tricky, so I decided to document the process so others don’t have to go through the same pain I did.

First, some caveats:

  • I’m using NeoVim. If you use plain Vim, these instructions may or may not work for you.
  • You should use a plugin manager for NeoVim, I use vim-plug, but the syntax is similar across the alternatives (the LanguageClient-neovim installation instructions may be of use for you).
  • I’m assuming basic knowledge of NeoVim and the command line. If you’re comfortable in the terminal, you’re probably pretty bored by now anyway.

Let’s begin with a minimal configuration that loads the LanguageClient-neovim plugin and enables it for JavaScript files.

First, we need to install a Language Server for JavaScript, we’ll use javascript-typescript-langserver which you can install via Yarn by running yarn global add javascript-typescript-langserver (or npm install -g javascript-typescript-langserver if you’re still on npm).

Now we create a minimal configuration file, which you should save to $HOME/.config/nvim/init.vim:

call plug#begin()
" LanguageClient plugin
Plug 'autozimu/LanguageClient-neovim', { 'do': ':UpdateRemotePlugins' }
call plug#end()

" Automatically start language servers.
let g:LanguageClient_autoStart = 1

" Minimal LSP configuration for JavaScript
let g:LanguageClient_serverCommands = {}
if executable('javascript-typescript-stdio')
  let g:LanguageClient_serverCommands.javascript = ['javascript-typescript-stdio']
  " Use LanguageServer for omnifunc completion
  autocmd FileType javascript setlocal omnifunc=LanguageClient#complete
else
  echo "javascript-typescript-stdio not installed!\n"
  :cq
endif

If you load up a JavaScript file in vim now (make sure you ran :PlugInstall to install the plugins) you’ll see … nothing special. However, if you invoke omni completion (via <C-x><C-u>), you’ll see the completion is much more intelligent than the default:

Smart omni completion in vim via language server

Other than smarter omni completion, this minimal setup also provides an error marker in the gutter indicating invalid syntax. These are both pretty nice, but really only scratch the surface of what we can do here. There is almost nothing is enabled by default, so let’s setup a few convenience mappings by adding the following to then end of init.vim:

" <leader>ld to go to definition
autocmd FileType javascript nnoremap <buffer>
  \ <leader>ld :call LanguageClient_textDocument_definition()<cr>
" <leader>lh for type info under cursor
autocmd FileType javascript nnoremap <buffer>
  \ <leader>lh :call LanguageClient_textDocument_hover()<cr>
" <leader>lr to rename variable under cursor
autocmd FileType javascript nnoremap <buffer>
  \ <leader>lr :call LanguageClient_textDocument_rename()<cr>

Now we can use the Language Server to find object definitions, types, and intelligently rename things. The rename is my personal favorite, since it is smarter than a normal find and replace, and respects variable scope:

Renaming a variable in vim via language server

This is pretty awesome, but so far it’s not much different than what was possible with other plugins like Tern for Vim. One of the exciting things about the Language Server Protocol is that it’s language agnostic. There are already implementation for several programming languages, and we get to leverage the same configuration once we’ve got our initial setup.

If I get tricked into coding python again, I can add support into NeoVim by installing the Python Language Server (via pip install python-language-server), and adding the following to init.vim:

let g:LanguageClient_serverCommands.python = ['pyls']

" Map renaming in python
autocmd FileType python nnoremap <buffer>
  \ <leader>lr :call LanguageClient_textDocument_rename()<cr>

Now NeoVim will load up the language server when opening python files, so I have access to the same features as when I edit JavaScript. Feature support varies by Language Server, but most support syntax checking, autocompletion, and renaming. A few even support code formatting as well.

LanguageClient-neovim is a pretty awesome plugin, there are a two integrations that are worth setting up:

Adding this to your init.vim is pretty easy:

" Put this in your plugin section:
" Fuzzy selection
Plug 'junegunn/fzf'
" IDE-like autocompletion without
Plug 'roxma/nvim-completion-manager'

" Put this outside of the plugin section
" <leader>lf to fuzzy find the symbols in the current document
autocmd FileType javascript nnoremap <buffer>
  \ <leader>lf :call LanguageClient_textDocument_documentSymbol()<cr>

Using fuzzy find to search through symbols is especially nice when some idiot decides it’s a good idea to have thousands of lines of code in a single file:

Fuzzy-finding a symbol in vim via language server

If you’re curious, I keep all my configuration files on GitHub. Let me know if you come up with any improvements!