Skip to content

Instantly share code, notes, and snippets.

@kyokley
Last active September 2, 2021 12:47
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kyokley/0d7bb03eede831bea3fa to your computer and use it in GitHub Desktop.
Save kyokley/0d7bb03eede831bea3fa to your computer and use it in GitHub Desktop.
Preventing saving for various errors in VIM

Preventing saving for various errors in VIM (Buffer Pre-write Hook Part 2)

Introduction

This is a continuation of my buffer pre-write hook series. Check out the previous gist to follow the progression.

Have you ever tried running a file only to be stopped by "<<<<<<<"?

Wouldn't it be nice to be able to automatically run your code through pyflakes before actually saving it? Maybe even raise an error for showstopping bugs in your code?

Tired of being yelled at by angry coworkers for accidentally leaving trailing whitespace in your files?

In my VIM status bar, I like to have indicators for things like trailing whitespace, version control conflict markers, and pyflakes errors. These are issues that should normally be resolved before checking in code. Being human, I sometimes forget to handle these issues. Fortunately, I have a computer that never forgets.

This gist hopes to demonstrate how to fix these issues by forcing a pre-write hook to make sure your code is clean before saving it. (This is basically an extension of my gist about handling whitespace. I realized whitespace wasn't the only thing that should block a save.)

Getting Started

From a high level, what we want to do is run through a serious of checks and raise exceptions if the checks fail. Should be simple and for the most part, it is. To begin, add the following to your vimrc

function! RaiseExceptionForUnresolvedErrors()

endfunction
autocmd BufWritePre * call RaiseExceptionForUnresolvedErrors()

This should be pretty self-explanatory. We're defining a function called RaiseExceptionForUnresolvedErrors and calling it before any buffer is written. From here, we will add the various checks between the function/endfunction lines.

Check for Version Control Conflict Markers

Checking for version control markers is pretty simple. We just need to come up with a regex to match the markers. I find that

    ^[<=>]{7}( .*|$)

works pretty well. Next, we wrap it in a search function and add it to RaiseExceptionForUnresolvedErrors.

function! RaiseExceptionForUnresolvedErrors()
    if search('\v^[<=>]{7}( .*|$)', 'nw') != 0
        throw 'Found unresolved conflicts'
    endif
    ...

Again, this should be pretty straightforward. I've added \v to the regex pattern to trigger "very magic" mode and passed the flags 'nw' to the search function. Check the VIM help pages for more information on those.

Checking for trailing whitespace

Similar to the check for version control markers, the method here will be to write a regex to match trailing whitespace.

    \s+$

Like before, I will wrap the regex in a search function and throw an error on failure.

function! RaiseExceptionForUnresolvedIssues()
    ...
    if search('\s\+$', 'nw') != 0
        throw 'Found trailing whitespace'
    endif
    ...

Notice, I don't use a "very magic" search pattern here. It's not worth the extra keystroke :-)

Checking python files for syntax errors

I like using the syntastic plugin with VIM. This plugin displays any errors from pyflakes for the current file. However, it can only update the errors AFTER the file has been written. My goal is to prevent writing a bad file in the first place. This ends up being slightly trickier than checking for the other issues but it should still be possible.

TL;DR

My method here is to make a copy of the current buffer and pipe it into pyflakes through stdin. I overwrite the temporary buffer with the result from pyflakes and begin checking for the errors that I'm interested in. If you're not interested in the "magic" to make that happen, skip below.

Step 1: Copy the current buffer into a temporary buffer

let s:file_name = expand('%:t')
%yank p
new
0put p
$,$d

The first thing I do is grab the name of the current file. That'll be useful in the next step. Next, I yank the entire buffer into register p, create a new split, and paste register p into it. The last line here is a little bit of clean up to trim the blank line from the end of the new buffer.

Step 2: Get the output from pyflakes

%!pyflakes
exe '%s/<stdin>/' . s:file_name . '/e'
unlet! s:file_name

The first line takes our newly created buffer and pipes it into pyflakes. Because we're technically sending the buffer to pyflakes through stdin, pyflakes doesn't actually know the name of the file when reporting errors. So, the second line replaces any references to stdin with the file name we saved in the previous step.

Step 3: Find badness

Pyflakes can search for all sorts of errors. However, I'm only really interested in certain ones. In particular, undefined name, unexpected indent, and invalid syntax should be showstoppers. There's nothing stopping you from adding or removing check to suit your style.

The basic form I'm using is search for error name. If we find that error, store the line number, get the message from that line, destroy the temporary buffer, and raise the error message.

This could look like the following:

let s:un_res = search('undefined name', 'nw')
if s:un_res != 0
    let s:message = 'Syntax error! ' . getline(s:un_res)
    bd!
    throw s:message
endif

let s:ui_res = search('unexpected indent', 'nw')
if s:ui_res != 0
    let s:message = 'Syntax error! ' . getline(s:ui_res)
    bd!
    throw s:message
endif

let s:is_res = search('invalid syntax', 'nw')
if s:is_res != 0
    let s:message = 'Syntax error! ' . getline(s:is_res)
    bd!
    throw s:message
endif

Assuming, we've made it through all of that, we can safely destroy our temporary buffer with one final

bd!

Some final housekeeping

Obviously, it doesn't really make sense to run non-python files through pyflakes, so I wrap everything in a filetype check. Also, I've prepended the 'silent' keyword to a couple of commands. VIM can be chatty sometimes so it just makes everything cleaner.

Thus, we end up with the following:

if &filetype == 'python'
    let s:file_name = expand('%:t')

    silent %yank p
    new
    silent 0put p
    silent $,$d
    silent %!pyflakes
    silent exe '%s/<stdin>/' . s:file_name . '/e'
    unlet! s:file_name

    let s:un_res = search('undefined name', 'nw')
    if s:un_res != 0
        let s:message = 'Syntax error! ' . getline(s:un_res)
        bd!
        throw s:message
    endif

    let s:ui_res = search('unexpected indent', 'nw')
    if s:ui_res != 0
        let s:message = 'Syntax error! ' . getline(s:ui_res)
        bd!
        throw s:message
    endif

    let s:is_res = search('invalid syntax', 'nw')
    if s:is_res != 0
        let s:message = 'Syntax error! ' . getline(s:is_res)
        bd!
        throw s:message
    endif

    bd!
endif

Result

Altogether my addition to my vimrc looks like this

function! RaiseExceptionForUnresolvedErrors()
    if search('\v^[<=>]{7}( .*|$)', 'nw') != 0
        throw 'Found unresolved conflicts'
    endif
    if search('\s\+$', 'nw') != 0
        throw 'Found trailing whitespace'
    endif
    if &filetype == 'python'
        let s:file_name = expand('%:t')

        silent %yank p
        new
        silent 0put p
        silent $,$d
        silent %!pyflakes
        silent exe '%s/<stdin>/' . s:file_name . '/e'
        unlet! s:file_name

        let s:un_res = search('undefined name', 'nw')
        if s:un_res != 0
            let s:message = 'Syntax error! ' . getline(s:un_res)
            bd!
            throw s:message
        endif

        let s:ui_res = search('unexpected indent', 'nw')
        if s:ui_res != 0
            let s:message = 'Syntax error! ' . getline(s:ui_res)
            bd!
            throw s:message
        endif

        let s:is_res = search('invalid syntax', 'nw')
        if s:is_res != 0
            let s:message = 'Syntax error! ' . getline(s:is_res)
            bd!
            throw s:message
        endif

        bd!
    endif
endfunction
autocmd BufWritePre * call RaiseExceptionForUnresolvedErrors()

An exception will be thrown when attempting to write any file containing unresolved conflict markers, trailing whitespace, or pyflakes errors. One of the reasons that I really like this method is that if you want to force the write to succeed anyway, you still have that ability. This can be accomplished by issuing the write command with noautocmd.

:noautocmd w

Or,

:noa w

Next Steps

Please check out the other gists in the series

@bbelderbos
Copy link

Can't star this enough ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment