Learn Vimscript the Hard Way

Toggling

In one of the first chapters we talked about how to set options in Vim. For boolean options we can use set someoption! to "toggle" the option. This is especially nice when we create a mapping for that command.

Run the following command:

:nnoremap <leader>N :setlocal number!<cr>

Try it out by pressing <leader>N in normal mode. Vim will toggle the line numbers for the current window off and on. Creating a "toggle" mapping like this is really handy, because we don't need to have two separate keys to turn something off and on.

Unfortunately this only works for boolean options. If we want to toggle a non-boolean option we'll need to do a bit more work.

Toggling Options

Let's start by creating a function that will toggle an option for us, and a mapping that will call it. Put the following into your ~/.vimrc file (or a separate file in ~/.vim/plugin/ if you prefer):

nnoremap <leader>f :call FoldColumnToggle()<cr>

function! FoldColumnToggle()
    echom &foldcolumn
endfunction

Write and source the file, then try it out by pressing <leader>f Vim will display the current value of the foldcolumn option. Go ahead and read :help foldcolumn if you're unfamiliar with this option.

Let's add in the actual toggling functionality. Edit the code to look like this:

nnoremap <leader>f :call FoldColumnToggle()<cr>

function! FoldColumnToggle()
    if &foldcolumn
        setlocal foldcolumn=0
    else
        setlocal foldcolumn=4
    endif
endfunction

Write and source the file and try it out. Each time you press it Vim will either show or hide the fold column.

The if statement simply checks if &foldcolumn is truthy (remember that Vim treats the integer 0 as falsy and any other number as truthy). If so, it sets it to zero (which hides it). Otherwise it sets it to four. Pretty simple.

You can use a simple function like this to toggle any option where 0 means "off" and any other number is "on".

Toggling Other Things

Options aren't the only thing we might want to toggle. One particularly nice thing to have a mapping for is the quickfix window. Let's start with the same skeleton as before. Add the following code to your file:

nnoremap <leader>q :call QuickfixToggle()<cr>

function! QuickfixToggle()
    return
endfunction

This mapping doesn't do anything yet. Let's transform it into something slightly more useful (but not completely finished yet). Change the code to look like this:

nnoremap <leader>q :call QuickfixToggle()<cr>

function! QuickfixToggle()
    copen
endfunction

Write and source the file. If you try out the mapping now you'll see that it simply opens the quickfix window.

To get the "toggling" behavior we're looking for we'll use a quick, dirty solution: a global variable. Change the code to look like this:

nnoremap <leader>q :call QuickfixToggle()<cr>

function! QuickfixToggle()
    if g:quickfix_is_open
        cclose
        let g:quickfix_is_open = 0
    else
        copen
        let g:quickfix_is_open = 1
    endif
endfunction

What we've done is pretty simple -- we're simply storing a global variable describing the open/closed state of the quickfix window whenever we call the function.

Write and source the file, and try to run the mapping. Vim will complain that the variable is not defined yet! Let's fix that by initializing it once:

nnoremap <leader>q :call QuickfixToggle()<cr>

let g:quickfix_is_open = 0

function! QuickfixToggle()
    if g:quickfix_is_open
        cclose
        let g:quickfix_is_open = 0
    else
        copen
        let g:quickfix_is_open = 1
    endif
endfunction

Write and source the file, and try the mapping. It works!

Improvements

Our toggle function works, but has a few problems.

The first is that if the user manually opens or closes the window with :copen or :cclose our global variable doesn't get updated. This isn't really a huge problem in practice because most of the time the user will probably be opening the window with the mapping, and if not they can always just press it again.

This illustrates an important point about writing Vimscript code: if you try to handle every single edge case you'll get bogged down in it and never get any work done.

Getting something that works most of the time (and doesn't explode when it doesn't work) and getting back to coding is usually better than spending hours getting it 100% perfect. The exception is when you're writing a plugin you expect many people to use. In that case it's best to spend the time and make it bulletproof to keep your users happy and reduce bug reports.

Restoring Windows/Buffers

The other problem with our function is that if the user runs the mapping when they're already in the quickfix window, Vim closes it and dumps them into the last split instead of sending them back where they were. This is annoying if you just want to check the quickfix window really quick and get back to working.

To solve this we'll introduce an idiom that comes in handy a lot when writing Vim plugins. Edit your code to look like this:

nnoremap <leader>q :call QuickfixToggle()<cr>

let g:quickfix_is_open = 0

function! QuickfixToggle()
    if g:quickfix_is_open
        cclose
        let g:quickfix_is_open = 0
        execute g:quickfix_return_to_window . "wincmd w"
    else
        let g:quickfix_return_to_window = winnr()
        copen
        let g:quickfix_is_open = 1
    endif
endfunction

We've added two new lines in this mapping. One of them (in the else clause) sets another global variable which saves the current window number before we run :copen.

The second line (in the if clause) executes wincmd w with that number prepended as a count, which tells Vim to go to that window.

Once again our solution isn't bulletproof, because the user might open or close new split between runs of the mapping. Even so, it handles the majority of cases so it's good enough for now.

This strategy of manually saving global state would be frowned upon in most serious programs, but for tiny little Vimscript functions it's a quick and dirty way of getting something mostly working and moving on with your life.

Exercises

Read :help foldcolumn.

Read :help winnr()

Read :help ctrl-w_w.

Read :help wincmd.

Namespace the functions by adding s: and <SID> where necessary.