Vim Line Numbers

Posted on Sat 07 April 2018 in Software Development

If you want your Vim line numbers to be relative and/or not relative at the correct times, I recommend installing the myusuf3/numbers.vim plugin.

The Problem

When editing text in a Vim window, I use relative numbers to help me use motions across text relative to my cursor. However, when I have multiple windows open, relative numbers look pretty weird in windows that I am not currently editing. It would be nice for Vim to intelligently alternate between relativenumber and norelativenumber based on my Vim cursor location. Base Vim does not have this capability, so we have three options:

  1. Accept a suboptimal workflow
  2. Wrap our own solution in our .vimrc
  3. Find a Plugin

I lived with option 1 for a while, but eventually grew too annoyed. I then tried researching plugins, but thought I understood the problem well-enough to write my own solution. So I went straight to option 2 and tried wrapping my own solution.

Wrapping my own solution

The following code represents my original solution:

 1 function! ToggleRelativeNumber()
 2   if &rnu
 3     set norelativenumber
 4   else
 5     set relativenumber
 6   endif
 7 endfunction
 8 
 9 function! RNUInsertEnter()
10   if &rnu
11     let w:line_number_state = 'rnu'
12     set norelativenumber
13   else
14     let w:line_number_state = 'nornu'
15   endif
16 endfunction
17 
18 function! RNUInsertLeave()
19   if w:line_number_state == 'rnu'
20     set relativenumber
21   else
22     set norelativenumber
23     let w:line_number_state = 'nornu'
24   endif
25 endfunction
26 
27 function! RNUWinEnter()
28   if exists('w:line_number_state')
29     if w:line_number_state == 'rnu'
30       set relativenumber
31     else
32       set norelativenumber
33     endif
34   else
35     set relativenumber
36     let w:line_number_state = 'rnu'
37   endif
38 endfunction
39 
40 function! RNUWinLeave()
41   if &rnu
42     let w:line_number_state = 'rnu'
43   else
44     let w:line_number_state = 'nornu'
45   endif
46   set norelativenumber
47 endfunction
48 
49 " autocmd that will set up the w:created variable
50 autocmd VimEnter * autocmd WinEnter * let w:created=1
51 autocmd VimEnter * let w:created=1
52 set number relativenumber
53 augroup rnu_nu
54   autocmd!
55   "Initial window settings
56   autocmd WinEnter * if !exists('w:created') |
57         \setlocal number relativenumber |
58         \endif
59   autocmd User Startified setlocal number relativenumber
60   " Don't have relative numbers during insert mode
61   autocmd InsertEnter * :call RNUInsertEnter()
62   autocmd InsertLeave * :call RNUInsertLeave()
63   " Set and unset relative numbers when buffer is active
64   autocmd WinEnter * :call RNUWinEnter()
65   autocmd WinLeave * :call RNUWinLeave()
66 augroup end

The good

The solution worked for most windows and tabs, most of the time.

The bad

The code is a bit involved and it takes a little time to explain to others.

  1. It relies on window-local variables (w:line_number_state, etc). These exacerbate Vim's already-difficult state-management woes.
  2. Several global functions are defined
  3. There are some quirks I don't fully understand around the creation of variables during Vim startup (hence lines 50 and 51).

Despite these mild downsides, I was pretty proud that the solution mostly worked. That is, until I wasn't.

The back-breaking straw

My custom solution did not work appropriately with some of my plugins. Namely, it didn't play well with majutsushi/tagbar, which I use frequently enough for this feature-dearth to become royally annoying. Therefore, after learning the ins-and-outs of window-specific variables and every Vim autocmd, I went back to the Plugin ecosystem to see if I'd missed anything...

The Game-Changing Plugin

Turns out a wonderful developer already solved this problem for me. Assuming you use junegunn/vim-plug to manage your plugins, place the following code in your .vimrc

call plug#begin('~/.vim/plugged')
" Relative Numbering
Plug 'myusuf3/numbers.vim'
" Put the rest of your plugins below...
call plug#end()

" Now, exclude the plugins you don't want numbers to deal with
let g:numbers_exclude = ['startify', 'gundo', 'vimshell']

This will give you a great editing experience. See below for a screencast:

numbers.vim screencast

Conclusion

Numbers.vim provides a usable line-numbering solution with minimal required configuration. I regret nothing about my bespoke journey, but I'm glad that Numbers.vim is my destination.