5

Since importing a package and not using it in Go is a compilation error, fixing the import manually can be a bit of a pain while debugging, so I'm currently using this to fix it up automatically before saving:

" Filter all lines through cmd, and silently undo it if there's an error
fun! s:write_cmd(cmd)
    let l:save = winsaveview()
    keepjumps silent %!goimports
    if v:shell_error != 0
        normal! u
    endif
    call winrestview(l:save)
endfun


augroup ftype_go
    autocmd!

    autocmd BufWritePre *.go call s:write_cmd('goimports')
augroup end

This works, but has at least two problems:

  • Pressing u undoes this.
  • :!! now runs goimports (and not, for example, :!go run % which I manually typed).
  • Perhaps others side-effects I haven't noticed yet?

How can I "transparently" filter my buffer to an external command (meaning it won't cause any side-effects)?

  • 1
    Perhaps you should clarify what you mean by "side effects"? For example, I personally would consider that if u didn't undo the last action, then that action had a very undesirable side effect (irreversible change). But it seems that you think u being able to undo your change is a side effect...? – Wildcard Apr 1 '16 at 3:05
  • 2
    @Wildcard If I do :set fileencoding=utf16, :w, and then press u, then I don't "undo" the encoding change, I will undo the last change I made to the buffer. This is similar. To me, the goimports command is an unimportant detail in the process of writing the buffer to disk. It's not an "action". I don't care about it. I never need to see it, care about it, or undo it, just like I don't need to see the fileencoding and many other details. – Martin Tournoij Apr 1 '16 at 3:16
  • 1
    vim-go provides :GoImports which will automatically discards/add import path based on the code. You can set it so goimports is ran on save by adding let g:go_fmt_command = "goimports" to your vimrc file. – Peter Rincker Apr 1 '16 at 14:48
3

For removing :!goimports from the history, use systemlist:

call setline(1, systemlist('goimports', getline(0, line('$'))))

The problem here is that if there are fewer lines after goimports, some garbage will remain at the end.

For the undo problem, use undojoin:

If you want to write a function or script that doesn't create a new undoable
change but joins in with the previous change use this command:

                                                :undoj :undojoin E790
:undoj[oin]             Join further changes with the previous undo block.
                        Warning: Use with care, it may prevent the user from
                        properly undoing changes.  Don't use this after undo
                        or redo.
                        {not in Vi}

This is most useful when you need to prompt the user halfway through a change.
For example in a function that calls getchar().  Do make sure that there was
a related change before this that you must join with.

This doesn't work by itself, because the next key press will start a new
change again.  But you can do something like this: 

        :undojoin | delete

After this an "u" command will undo the delete command and the previous
change.

So, something like:

fun! s:write_cmd(cmd)
    let l:save = winsaveview()
    let l:post_imports = systemlist('goimports', getline(0, line('$')))
    let l:num_lines = len(l:post_imports)
    let l:cmds = ['undojoin', 'call setline(1, l:post_imports)']
    if l:num_lines < line('$')
        let l:cmds += [l:num_lines + 1 . ',$ delete']
    endif
    if v:shell_error == 0
        keepjumps silent exec join(l:cmds, '|')
    endif
    call winrestview(l:save)
endfun
  • This errors out with invalid range: 12$,delete. I tried to fix it, but wasn't really able to get this to work in <10 minutes... – Martin Tournoij Jan 4 '17 at 0:02
  • @Carpetsmoker took me a while to remember what this was doing - the error is from me blindly applying l:num_lines + 1 . '$,delete', when I should be doing it only if the number of lines returned by goimports is less than the number of lines actually in the file. (And it should have been l:num_lines + 1 . ',$ delete, of course.) – muru Jan 5 '17 at 6:38
2

This is actually pretty tricky to do. I've been using the vim-go plugin for a while*, which has the same problems as described in the question.

However, looking at the source I found that I can set a g:go_fmt_experimental to enable some code which attempts to fix this.

Here is a modified version which removes some of the Go specific stuff and vim-go utility functions, so it can be reused for other formatters and such. It also avoids having to use a temporary file for the unsaved buffer by using stdin.

See the comments for details as to how this works. It's also not perfect, since not everything is always restored (again, see the comments). But it looks like this is the best that's possible at the moment.

" Adapted from vim-go: https://github.com/fatih/vim-go/blob/master/LICENSE
"
"  we have those problems :
"  http://stackoverflow.com/questions/12741977/prevent-vim-from-updating-its-undo-tree
"  http://stackoverflow.com/questions/18532692/golang-formatter-and-vim-how-to-destroy-history-record?rq=1
"
"  The below function is an improved version that aims to fix all problems.
"  it doesn't undo changes and break undo history.  If you are here reading
"  this and have VimL experience, please look at the function for
"  improvements, patches are welcome :)
function! s:write_cmd(cmd) abort
    " Using winsaveview to save/restore cursor state has the problem of
    " closing folds on save:
    "   https://github.com/fatih/vim-go/issues/502
    " One fix is to use mkview instead. Unfortunately, this sometimes causes
    " other bad side effects:
    "   https://github.com/fatih/vim-go/issues/728
    " and still closes all folds if foldlevel>0:
    "   https://github.com/fatih/vim-go/issues/732
    let l:curw = {}
    try
        mkview!
    catch
        let l:curw = winsaveview()
    endtry

    " save our undo file to be restored after we are done. This is needed to
    " prevent an additional undo jump due to BufWritePre auto command and also
    " restore 'redo' history because it's getting being destroyed every
    " BufWritePre
    let tmpundofile = tempname()
    exe 'wundo! ' . tmpundofile

    " Write current unsaved buffer to a temp file
    let l:tmpname = tempname()
    call writefile(getline(1, '$'), l:tmpname)
    if s:isWindows()
        let l:tmpname = tr(l:tmpname, '\', '/')
    endif

    let out = system(a:cmd, getline(1, '$'))[:-2]

    if v:shell_error > 0
        echoerr out
        return
    end

    try
        silent undojoin
    catch
        " Silently ignore
    endtry


    " set buffer text
    silent 0,$delete
    call setline(1, split(out, '\n'))

    " restore our undo history
    silent! exe 'rundo ' . tmpundofile
    call delete(tmpundofile)

    " Restore our cursor/windows positions, folds, etc.
    if empty(l:curw)
        silent! loadview
    else
        call winrestview(l:curw)
    endif
endfunction

" IsWin returns 1 if current OS is Windows or 0 otherwise
function! s:isWindows() abort
    let win = ['win16', 'win32', 'win64', 'win95']
    for w in win
        if (has(w))
            return 1
        endif
    endfor

    return 0
endfunction

augroup write_Cmd
    autocmd BufWritePre *.go call s:write_cmd('goimports')
augroup end

*: I was apprehensive about using it at first due to bad experiences with some other language plugins, but vim-go is much more sane.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.