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
