Friday, October 20, 2006

Making ZZ Behave in Vim

Update Apr 4, 2008: I was recently reminded of this post and thought I should mention that this particular solutions sucks. Although it won't break anything, it doesn't work well. Don't waste your time :) Cheers.

I've gotten quite used to the idea of hitting ZZ in Vim to kill the window I'm currently working in. The problem is that I'm so used to the idea, that I hit it almost automatically when I'm done with a particular buffer and there is only one window left. The result is that the Vim session is closed completely whether there is only one listed buffer or not.

My solution to this was not to find the probably existing command or setting built into Vim that corrects this problem, but rather to write a function that customizes the behavior of the ZZ command. This would be the first time I'd ever written a non-trivial function in pure Vim script, so I thought I'd give it a shot. The following function is the result. I've put lots of comments in there so that you can follow if you are new to Vim scripting like I am.

function! BehaveZZ()
    " Get the number of *listed* buffers.
    let highbuf = bufnr("$")
    let buflist = []
    let i = 1
    while (i <= highbuf)
        "Skip unlisted buffers.
        if (bufexists(i) != 0 && buflisted(i))
            call add(buflist, i)
        endif
        let i = i + 1
    endwhile
    let bufcount = len(buflist)
    if (bufcount == 1)
        if (bufname("%") == "")
            " This buffer is unnamed (has no associated file).
            if (&modified)
                " Give option to save modifications.
                let choice = input("Lose modifications? [Enter=yes]: ")
                if (choice == "")
                    set nomodified
                else
                    echo "ZZ action aborted..."
                    return
                endif
            else
                " The buffer has no modifications. Just do default ZZ.
                execute "x"
            endif
        else
            " There is only one listed buffer and it is named. In this case, 
            " the standard ZZ works just fine, so do that.
            execute "x"
        endif
    elseif (getbufvar(bufnr("%"), "&buftype") != "")
        " This buffer is a "special" buffer.
        execute "bdelete"
    elseif (bufname("%") == "")
        " This buffer is unnamed (has no associated file).
        if (&modified)
            " Give option to save modifications.
            let choice = input("Lose modifications? [Enter=yes]: ")
            if (choice == "")
                set nomodified
            else
                echo "ZZ action aborted..."
                return
            endif
        endif
        if (winnr("$") > 1)
            " There are multiple windows open. Just do a normal ZZ.
            execute "x"
        else
            execute "buffer! " . bufnr("#")
        endif
    else
        " This is a named buffer. 
        if (&modified)
            execute "write"
        endif
        if (winnr("$") > 1)
            " There are multiple windows. Just do normal ZZ.
            execute "x"
        else
            " There is only one window, but multiple listed buffers.
            let curbuf = bufnr("%")
            " If we have a 'last visited' buffer, go there. Else bnext.
            if (bufnr("#") != -1)
                execute "buffer! " . bufnr("#")
            else
                execute "bnext"
            endif
            execute "bdelete" . curbuf
        endif
    endif
endfunction
command! ZZ call BehaveZZ()

Now at the time of this writing, the above function has had very limited testing, so if you want to use it, beware. This is what the function does.

  • If there are multiple windows open, just close the current one. (Same as default ZZ)
  • If there is only one window open, but multiple buffers, delete the current buffer and switch to another one.
  • If there is only one window, and only one listed buffer, exit the session. (Same sd default ZZ)
  • If the current buffer is unnamed (ie. has no filename associated with it), confirm loss of modifications. If user presses just Enter, then discard modifications and delete the buffer. Else abort.

One behaviour of the default Vim that I found odd is a result of opening multiple files from the command line at once like vim file1 file2. When you do this, and try to close one you get a E173: 1 more file to edit message, and nothing happens at all. It however gives no such warnings if you open only one file, then open a second with the :e file2 command from inside the editor. This time it just closes the whole session if there is only one window open. In both of these cases, my function will close the currently active buffer and switch to one of the other buffers in the buffer list.

I think I've handled most special cases where a buffer is special like a help buffer, or a scratch buffer too. Initially I made it so that if the current buffer was unnamed and modified then the modifications would just be discarded, but that made me nervous so I added a one key confirmation just to be safe. The whole function turned out to be a lot more complex than I thought it would (not to mention longer). Things like special buffers gave me a lot of grief. Being new to this, the whole snippet probably ended up being more complicated than it needed to be. If you see any bugs or optimizations, I'd be interested in hearing about them. Leave a comment after this post.

To use this function, drop it into your ~/.vimrc and either :call it manually or map a key to it like

:map ZZ :call BehaveZZ()<CR>
. That's the mapping I use and it simply replaces ZZ's normal behavior with the new one.

No comments:

Post a Comment