Gofmt is both a code formatter and a linter, a design decision that makes it harder than necessary to integrate it with Vim. In practice, it means that things can go three ways:
- your code is perfect, nothing happens,
- your code has formatting issues, it is reformatted,
- your code has syntax issues, a list of errors is output.
For example, we could naively try to use gofmt for gq and friends by way of :help 'formatprg' but the chances of eventually overwritting our code with crap like:
<standard input>:4:2: expected statement, found '.'
<standard input>:5:3: expected '}', found 'EOF'
are too high. Like in your case, we can do u to undo but that's not fun. I guess we will have to work around gofmt's bad design.
First step: switch to :help BufWritePre. We have seen that gofmt can handle stdin, which allows us to format the buffer as well as the file. That is handy because formatting the file after it was written writes the file a second time for no good reason and forces us to reload it in Vim… and all that seems wasteful. :help BufWritePost is best kept for things that don't affect Vim's state.
function! GoFmt()
echomsg "hello"
endfunction
command! GoFmt call GoFmt()
augroup go_autocmd
autocmd BufWritePre *.go GoFmt
augroup END
Second step: filter the whole buffer through gofmt.
function! GoFmt()
silent %!gofmt
endfunction
- Best case scenario: nothing happens or the buffer is overwritten with the formatted content.
- Worst case scenario: the whole buffer is replaced with an error report.
Third step: "handle" the worst case scenario with a basic undo. If the external command returns an error, we can get it via :help v:shell_error and do what needs to be done.
function! GoFmt()
silent %!gofmt
if v:shell_error > 0
silent undo
endif
endfunction
Fourth step: try to keep the cursor in place.
function! GoFmt()
let saved_view = winsaveview()
silent %!gofmt
if v:shell_error > 0
silent undo
endif
call winrestview(saved_view)
endfunction
See :help winsaveview() and :help winrestview().
Fifth step: if applicable, create a quickfix list with the errors reported by gofmt. :help getline() gives us all the lines of the buffer—therefore all the errors—in a list of which we modify each item so that the file name is the current file name instead of the useless <standard input>. We give that list to :help :cexpr to create a quickfix list before undoing the filter.
function! GoFmt()
let saved_view = winsaveview()
silent %!gofmt
if v:shell_error > 0
cexpr getline(1, '$')->map({ idx, val -> val->substitute('<standard input>', expand('%'), '') })
silent undo
endif
call winrestview(saved_view)
endfunction
This step has a bit of a "draw the rest of the * howl" vibe but it really is just a simple :help substitute() in a simple :help map(). For the { foo, bar -> baz } syntax, see :help lambda.
Sixth and last step: open the quickfix window if there are any valid errors, with :help :cwindow.
function! GoFmt()
let saved_view = winsaveview()
silent %!gofmt
if v:shell_error > 0
cexpr getline(1, '$')->map({ idx, val -> val->substitute('<standard input>', expand('%'), '') })
silent undo
cwindow
endif
call winrestview(saved_view)
endfunction
