Coffee and Contemplation A developer blog

Language Server Client in neovim

What is language server protocol?

Development becomes a lot easier if all languages support features like autocomplete, linting, go-to-definition etc. within the editor of your choice. But this is a difficult task for editor devs. Each editor has to build support or integrate with tools that provide language support. Each of these tools behave in different ways making integration difficult. Language server protocol was defined by microsoft to put and end to this. LSP defines a standard protocol for tools(called language servers) to communicate with your editor.

Why lsp Image source: Visual Studio Code

What is a language server client?

A language server client is the part of your editor that interacts with a language server - Sometimes implemented as plugins, sometimes built-in.

What language clients are available for vim?

  1. vim-lsp
  2. LanguageClient-neovim
  3. coc.nvim
  4. ale
  5. Built-in language server in neovim

We’ll see how to setup the built-in language server in neovim in this blog post.

First, setup a decent looking theme

Onebuddy is an atom one inspired theme. Install the theme with vim-plug. Add this to your vimrc:

Plug 'tjdevries/colorbuddy.vim'
Plug 'Th3Whit3Wolf/onebuddy', { 'branch': 'main' }

Enable the theme by adding this to vimrc:

set termguicolors
colorscheme onebuddy

What features should we expect?

  1. Show errors in line
  2. Show warnings in line
  3. Show errors/warnings in a list
  4. Format file
  5. Jump to definition
  6. See documentation
  7. Code actions - Actions like “Extract to function”, “Import this module” - Yes, you can get that in vim.
  8. Autocomplete

The first 7 are really easy to achieve. Let us handle autocomplete in a separate post.

How to setup built-in lsp client for neovim?

neovim’s lsp client is built in lua. The configuration is also in lua. Usable configurations for common language servers are collected in neovim/nvim-lspconfig.

Install the plugin with:

Plug 'neovim/nvim-lspconfig'

Let us create a new file in .vim directory with all the lsp configurations. Name it lsp_config.lua. Now in your vimrc, add the following:

luafile ~/.vim/lsp_config.lua

In this file, add some configuration for common language servers

lspconfig = require'lspconfig'

lspconfig.pyls.setup{}
lspconfig.tsserver.setup{}
lspconfig.rust_analyzer.setup{}

That is it! You should start seeing diagnostics now. Ensure you have the right language servers installed. For example, for python language server, do

pip3 install 'python-language-server[all]'

Add some signs to the gutter

It would be nice to see some symbols to the left of the code when there are errors. Add these lines to your vimrc.

sign define LspDiagnosticsSignError text=🔴
sign define LspDiagnosticsSignWarning text=🟠
sign define LspDiagnosticsSignInformation text=🔵
sign define LspDiagnosticsSignHint text=🟢

Add some keybindings

We need some keybindings for actions like jump to definition. Add the following to your vimrc.

nnoremap <silent> gd    <cmd>lua vim.lsp.buf.definition()<CR>
nnoremap <silent> gi    <cmd>lua vim.lsp.buf.implementation()<CR>
nnoremap <silent> gr    <cmd>lua vim.lsp.buf.references()<CR>
nnoremap <silent> gD    <cmd>lua vim.lsp.buf.declaration()<CR>
nnoremap <silent> ge    <cmd>lua vim.lsp.diagnostic.set_loclist()<CR>
nnoremap <silent> K     <cmd>lua vim.lsp.buf.hover()<CR>
nnoremap <silent> <leader>f    <cmd>lua vim.lsp.buf.formatting()<CR>
nnoremap <silent> <leader>rn    <cmd>lua vim.lsp.buf.rename()<CR>

nnoremap <silent> <leader>a <cmd>lua vim.lsp.buf.code_action()<CR>
xmap <silent> <leader>a <cmd>lua vim.lsp.buf.range_code_action()<CR>

This will create the following mappings

Keybindings Action
gd Go to definition
gi Go to implementation
gr See references
gD Go to declaration
ge Show errors in loclist
gd Go to definition
space f FOrmat file
space r n Rename variable under cursor
space a Code actions - On normal mode and visual selections

Your end result should look like this: lsp screenshot

What I learned from doing dishes

I woke up today and was welcomed by a mountain of dishes in the sink. My first reaction was - “Screw that - I’ll just go back to bed”. So, after a couple more hours of sleep, I went back to find that the mountain hadn’t magically disappeared.

Now I had to tackle this problem. This is the strategy I followed

  1. Move everything out of the sink back into the kitchen.
  2. Take 5 utensils into the sink.
  3. Wash them.
  4. If there are more dishes left, goto 2.

That was it! The mountain of dishes were done.

This is how a typical Monday looks like too. You wake up - look at the mountin of tasks - try to do them all at once - fail miserably - wonder at the end of the week, what went wrong.

All you need to do is this - split the tasks into “5-utensil-size” tasks. Tackling smaller tasks is easy and far, far less intimidating.

I’ve started doing that now, and not digitally. Grabbed a notebook and started adding tasks in there and splitting them. The inspiration for this came from the Bullet journal method designed by Ryder Carroll.

Fuzzy finding in vim - vim + fzf

FZF is the center-piece of my vim configuration. It is another excellent piece of software by Junegunn Choi.

fzf is a command line fuzzy finder.

Now, that doesn’t sound like much. But the way it can integrate with your other tools will blow your mind. The repository for FZF is located here and has extensive documentation on how to use it. Follow these instructions to install it.

To see how it works, run this command:

vim $(find . | fzf)

find . will list all the files in the directory recursively. You pipe that output into fzf. Fuzzy search for the file of your choice and press enter. fzf will output that filename and that is passed as an argument to vim. Pretty neat, huh? fzf also has a multiselect mode which can be activated with fzf -m. You can select multiple entries by pressing TAB.

fzf has a plugin interface for using it in vim. Before installing and jumping into it, we need to lay some groundwork.

RipGrep

RipGrep is a faster replacement for grep written in rust. Output of some ripgrep commands will be piped into fzf to get our desired setup. Now, go install ripgrep. The binary is called rg.

Vim leader key

Vim can map a sequence of keys to an action. Leader is just another key in that sequence. For example, map <leader>q :q<CR> will map the sequence <leader> + q to :q <enter>. The default leader key is \. You can change the value of leader with :mapleader. I find space to be the best key for leader. It is accessible with both fingers and it isn’t really doing anything else in the normal mode. So, go ahead and add this line in your vimrc.

let mapleader = "\<Space>"

Install fzf.vim

We saw how to install plugins using vim-plug in another post.

Add these 2 lines to your vimrc(Between plug#begin and plug#end).

Plug 'junegunn/fzf', { 'do': { -> fzf#install() } }
Plug 'junegunn/fzf.vim'

Now, let us setup some config and keybindings for useful commands.

" use ripgrep to search for files
let $FZF_DEFAULT_COMMAND = 'rg -l ""'

map <C-p> :Files<CR>
map <C-b> :Buffers<CR>
map <Leader><Leader> :Commands<CR>
map <Leader>/ :execute 'Rg ' . input('Rg/')<CR>
map <Leader>l :BLines<CR>
map <Leader>gf :GF?<CR>

This will create the following mappings

Keybindings Action
Ctrl + p Fuzzy find files in the current working directory
Ctrl + b Fuzzy find open buffers
space space Fuzzy find from a list of all available commands
space / Search for some text within the working directory. Enter the text after pressing space /
space l Fuzzy search lines in the open file
space g f Fuzzy search for files that got added/changed after the last commit

You can find a list of commands here. See if anything tickles your fancy.

Searching for word under cursor

Something that I do pretty regularly is to search for the word under my cursor. This can be done by defining a vim function. I bind it to space G(That is an uppercase G).

command! -bang -nargs=* RgExact
  \ call fzf#vim#grep(
  \   'rg -F --column --line-number --no-heading --color=always --smart-case -- '.shellescape(<q-args>), 1,
  \   fzf#vim#with_preview(), <bang>0)

nmap <Leader>G :execute 'RgExact ' . expand('<cword>') <Cr>

Example

Vimming in 2020

If you use vim, you can use h,j,k,l keys to navigate instead of the arrow keys. You won’t have to move your hands away from the home row, and you’ll end up saving a lot of time.

One of my seniors in college told me this about vim. It sounded so incredibly ridiculous at that time, that I decided to give it a shot.

For the first couple of years, my .vimrc looked like this.

set nu
set cindent
syntax on

This enabled line numbers, indentation and added some syntax highligting to the code.

My real journey into vim started with this post titled “How I boosted my vim” by Vincent Driessen. It introduced me to vim’s plugin ecosystem and I found out there is more to vim than h,j,k,l.

10 years later, my (neo)vim looks like this: neovim screenshot

Basic setup

Depending on if you are using vim or neovim, startup file is different. For vim, it is ~/.vimrc. For neovim, it is ~/.config/nvim/init.vim. Add the following block into your startup file.

filetype plugin indent on
syntax on

set modeline
set encoding=utf-8
set termencoding=utf-8
set tabstop=4                   " a tab is four spaces
set expandtab                   " Use spaces to insert a <TAB>
set backspace=indent,eol,start  " allow backspacing over everything
                                " in insert mode
set autoindent                  " always set autoindenting on
set copyindent                  " copy the previous indentation
                                " on autoindenting
set number                      " always show line numbers
set shiftwidth=4                " number of spaces to use
                                " for autoindenting
set shiftround                  " use multiple of shiftwidth when
                                " indenting with '<' and '>'
set smarttab                    " insert tabs on the start
                                " of a line according to
                                " shiftwidth, not tabstop
set hlsearch                    " highlight search terms
set incsearch                   " show search matches as you type
set ignorecase                  " ignore case when searching
set smartcase                   " ignore case if search pattern is all
                                " lowercase, case-sensitive otherwise
set scrolloff=8                 " Start scrolling at the last 8 lines
set history=1000                " remember more commands and search history
set undolevels=1000             " 1000 levels of undo
set title                       " change the terminal's title
set noerrorbells                " don't beep
set wildmenu                    " Better command line completion
set wildmode=list:longest,full
set nobackup                    " no backup files
set nowritebackup
set noswapfile                  " no swap files
set nolazyredraw
set hidden
set cmdheight=2                 " Better display for messages
set updatetime=300
set inccommand=nosplit          " Preview for search and replace
                                " (neovim only)

set pastetoggle=<F2> " F2 Toggles set paste
au InsertLeave * set nopaste " Exit paste mode when leaving insert mode

" No shift for :
nnoremap ; :

" Navigate wrapped lines easily
nnoremap j gj
nnoremap k gk

" Remove highlight on ,/
nmap <silent> ,/ :nohlsearch<CR>

" Correct typos
aug FixTypos
    :command! WQ wq
    :command! Wq wq
    :command! QA qa
    :command! Qa qa
    :command! W w
    :command! Q q
aug end

If you would like to know more details about any of these settings, you can do a :h <command>. For eg:- :h autoindent

Plugins

vim-plug by Junegunn Choi is a good candidate for a plugin manager.

Install vim-plug.

For vim in osx, linux

curl -fLo ~/.vim/autoload/plug.vim --create-dirs \
    https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim

For neovim in osx, linux

sh -c 'curl -fLo "${XDG_DATA_HOME:-$HOME/.local/share}"/nvim/site/autoload/plug.vim --create-dirs \
       https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim'

For others, see here. When you start vim next, the vim-plug will be installed.

Now, let us install a plugin called lightline. The URL for lightline on github is https://github.com/itchyny/lightline.vim. You need to use the url slug for the plugin to install it; like shown below.

call plug#begin('~/.vim/plugged')

Plug 'itchyny/lightline.vim'

call plug#end()

To install the plugin, quit vim and run vim +PlugInstall. When you restart vim, you can see a better status line(the line the bottom of vim).

vim-plug provides some commands to manage plugins. Here are some I use often.

  1. PlugInstall - Installs all the listed plugins in vimrc.
  2. PlugUpdate - Updates all the listed plugins in vimrc.
  3. PlugClean - Removes unlisted plugins.

Context managers in python

Say, you have to open and process a file in python. You can do that using a function like this

def open_and_process(filename):
    file = open(filename)
    process(file)
    file.close()  # This is important

If an exception occurs in the process function, file.close() won’t be executed.

This pattern is the same for other types of resources too:

  1. Acquire resource
  2. Use resource
  3. Release resource

If something fails during the use phase, the resource won’t be released cleanly. There are a couple of ways to solve this.

  1. Use try...finally
    def open_and_process(filename):
     try:
         file = open(filename)
         process(file)
     finally:
         file.close()
    
  2. Python Context Managers.

You would have seen code in python that looks like this.

with open(filename) as word_file:
    words = word_file.readlines()

A Context Manager is a class which implements __enter__ and __exit__ functions

class context:
    def __init__(self):
        print("initializing")

    def __enter__(self):
        print("entering block")
        return(10)

    def __exit__(self, _type, _value, _trace):
        print("exiting block")


with context() as val:
    print("block")
    print(val)     # This is the return value of __enter__ function

# Output
> initializing
> entering block
> block
> 10
> exiting block

When entering the block, the __enter__ method is called. When exiting the block, the __exit__ method is called. The cleanup(like closing the file) can be done in the __exit__ method.

Luckily, python provides a contextlib class so that you won’t have to write a class with __enter__ and __exit__ methods. In the example below, code before yield will be executed before entering the block. Code after yield will be executed on exiting the block.

from contextlib import contextmanager

@contextmanager
def context():
    print("entering")
    try:
        yield 10
    finally:
        print("exiting")

with context() as var:
    print(var)  # this is the yielded value
    print("inside block")

# output
> entering
> 10
> inside block
> exiting

An interesting usecase for this is in mocking classes in tests. Here, we are building a calculator which calls an AdderService to perform addition. It calls the AdderService using an AdderServiceClient class.

class AdderServiceClient:
    def add(self, a, b):
        # This is an external API call. Throwing an exception to
        # indicate failure when called during tests
        raise Exception("This calls an external service "
                        "and should not be used in tests")

class Calculator():
    def __init__(self, adder):
        self.adder = adder

    def add(self, a, b):
        return self.adder.add(a, b)

def test_calculator_adds():
    calc = Calculator(AdderServiceClient())
    assert calc.add(1, 2) == 3

# This test fails with
> Exception: This calls an external service and should not be used in tests

Now, bring in some contextmanager magic here.

from contextlib import contextmanager

@contextmanager
def mocked_adder_service_client():

    # Define a function that can add without
    # calling an external service.
    # The _ here is a placeholder for self
    def mocked_add(_, a, b):
        return a + b

    # Take a backup of the add in AdderServiceClient
    real_add = AdderServiceClient.add
    # Replace the add in AdderServiceClient with our mock function
    AdderServiceClient.add = mocked_add

    yield

    # Cleanup. Put back the real add
    AdderServiceClient.add = real_add

def test_calculator_adds():
    calc = Calculator(AdderServiceClient())
    with mocked_adder_service_client():
        assert calc.add(1, 2) == 3

We have a passing test case now.