Distraction free writing in Vim

I use Vim as a plain text editor for everything. Apart from coding, I write blog posts in vim, create quick notes and draft design documents. Surprisingly, I haven’t customized it a lot and am very lean about the plugins. While keeping my setup austerely simple, I rarely lack any of the functionality.

However, recently I wondered if I can achieve a “distraction free” experience with Vim, like when you see a full screen text, nicely centered, and nothing else:

distrction-free

Distraction free

To achieve distraction free editing in Vim, one would have to:

The last item is trivial: set wrap, set lbr, map j gj, map k gk.

Hiding elements. Well, this may sound tricky if we want to really hide elements. But what we really want is to visually hide them. They will still be there, but won’t be visible. This can be easily achieved by changing foreground color of such elements to match background color.

Finally, margins. One seemingly simple way to make left margin is to use line numbers and folds, make them as wide as needed and colorize to match the background. Right margin can be achieved by limiting the number of columns. But in practice, it’s a very limited approach. Left margin can only be as wide as 20 columns (because each part, numbers and folds, can take up to 10 columns). And it makes it impossible to use numbers and folds in the editor.

An alternative would be to create fake split windows on the left, right, top and bottom, then make the splits invisible by colorizing them.

So, let’s implement it like that.

Implementation

Here’s a code snippet. Feel free to put it into your .vimrc:

let g:dfm_width = 80 "absolute width or percentage, like 0.7
let g:dfm_height = 0.8

let s:dfm_enabled = 0

function! ToggleDistractionFreeMode()
  let l:w = g:dfm_width > 1 ? g:dfm_width : (winwidth('%') * g:dfm_width)
  let l:margins = {
	\ "l": ("silent leftabove " . float2nr((winwidth('%') - l:w) / 2 - 1) . " vsplit new"),
	\ "h": ("silent rightbelow " . float2nr((winwidth('%') - l:w) / 2 - 1) . " vsplit new"),
	\ "j": ("silent leftabove " . float2nr(winheight('%') * (1 - g:dfm_height) / 2 - 1) . " split new"),
	\ "k": ("silent rightbelow " . float2nr(winheight('%') * (1 - g:dfm_height) / 2 - 1) . " split new"),
	\ }
  if (s:dfm_enabled == 0)
    let s:dfm_enabled = 1
    for key in keys(l:margins)
      execute l:margins[key] . " | wincmd " . key
    endfor
    for key in ['NonText', 'VertSplit', 'StatusLine', 'StatusLineNC']
      execute 'hi ' . key . ' ctermfg=bg ctermbg=bg cterm=NONE'
    endfor
    set wrap | set linebreak
    map j gj
    map k gk
  else
    let s:dfm_enabled = 0
    for key in keys(l:margins)
      execute "wincmd " . key . " | close "
    endfor
    set nowrap | set nolinebreak
    unmap j
    unmap k
  endif
endfunction

nmap <silent> <Leader>z :call ToggleDistractionFreeMode()<CR>

We got two global configuration variables, width and height. Height is a percentage of the window height. Width can be a percentage, but I wound it easier to have absolute value (so that the text would look the same in ultra-wide windows and half-screen panes).

Then, we need to remember the current state of the windows to toggle them properly.

Finally, we have a function that opens 4 split windows, changes highlighting to hide the elements, and maps the key in one case, while closing the windows and unmapping the keys in the other case.

Note, that this may not work with color schemes when no background color is defined. You might get E420 error. In this case, add hi Normal ctermbg=black when distraction-free mode gets enabled, so that the background color would be forced to be black.

I know that there exists a number of plugins for distraction free writing mode in Vim. Still, the solution above is much smaller, and perfectly fits my needs (maybe yours, too). Plus, learning new things is always fun, even if it requires touching the VimScript.

Oct 10, 2019

like   tweet  
rss   @me   </>me