4

I have the following code in a file:

function! s:TrimWhitespace() abort range

    " Save cursor position, because the following substitution will move it
    let save_cursor = getpos('.')
    " Check whether a range was passed to the function   
    if exists('a:firstline')
        execute 'keepjumps keeppatterns ' .
                    \ a:firstline . ',' . a:lastline . 's/\v\s+$//e'
    else
        keepjumps keeppatterns %s/\v\s+$//e
    endif
    " Put the cursor back where it was before the function was called
    call setpos('.', save_cursor)

endfunction
command! -range=% TrimWhitespace <line1>,<line2>call <SID>TrimWhitespace()

It defines the command :TrimWhitespace which calls a function with the same name, and removes trailing whitespace from the buffer.

The command accepts a range, which by default is 1,$ because it's defined with the argument -range=%, so that it can be called like this:

:TrimWhitespace           removes all trailing whitespace
:'<,'>TrimWhitespace      only in visual selection
:10,20TrimWhitespace      only between lines 10 and 20

It works as expected except for one thing: the cursor position is not saved and restored properly. No matter where the cursor is before the command is executed, it ends on the first line of the range.
I think the reason why it doesn't work is because when the cursor position is saved, it's already too late, the function has already moved the cursor to the first line of the range.

I thought maybe putting normal `` before and after the save like this: normal `` let save_cursor = getpos('.') normal `` ... would work, but it doesn't.

Another solution would be to save the position outside the function like this:

command! -range=% TrimWhitespace let g:save_cursor = getpos('.') |
            \ <line1>,<line2>call <SID>TrimWhitespace()

But I don't like polluting the global namespace with a variable.

From inside the function, is there a way to save the position which the cursor had just before the function was called?

Edit: following romainl's answer, I've changed the code like this:

function! s:TrimWhitespace() abort range

    " Check whether a range was passed to the function   
    if exists('a:firstline')
        execute 'keepjumps keeppatterns ' .
                    \ a:firstline . ',' . a:lastline . 's/\v\s+$//e'
    else
        keepjumps keeppatterns %s/\v\s+$//e
    endif
    " Restore the state of the window (useful for not losing cursor position)
    call winrestview(b:winview)

endfunction
command! -range=% TrimWhitespace let b:winview = winsaveview() |
            \ <line1>,<line2>call <SID>TrimWhitespace()

I completely forgot you could use a variable local to a buffer, plus winsaveview() seems to save more than just the cursor position (first line of the window for example, that's nice).

Edit 2: No need of a function

command! -range=% TrimWhitespace let b:winview = winsaveview() |
            \ execute 'keepjumps keeppatterns ' .
            \ <line1> . ',' . <line2> . 's/\s\+$//e' |
            \ call winrestview(b:winview) |
            \ unlet b:winview
3
  • Actually, letting a window-local option is not a good idea. Use a buffer-local one instead.
    – romainl
    Jan 8, 2016 at 20:26
  • Ok I will change the code, but why a window-local variable is not a good idea? In which scenario would it cause some issue?
    – saginaw
    Jan 8, 2016 at 21:02
  • 1
    Basically, I had b:winview in my original command and elsewhere but, when I yanked it for my answer, I thought that using a buffer variable for a window-related feature sounded dumb and replaced it everywhere with w:winview in my config. I quickly started to notice a weird behavior: switching to another buffer in the same window always landed me in strange places. I figured out why this afternoon: winsaveview() saved the window's state to a window variable so each buffer would share the same state. Using b:winview solves that.
    – romainl
    Jan 8, 2016 at 22:23

1 Answer 1

5

This is how I do:

" reformat selection
command! -buffer -range=% Format let b:winview = winsaveview() |
  \ execute <line1> . "," . <line2> . "!js-beautify -f - -j -t -s " . &shiftwidth |
  \ call winrestview(b:winview)

You could probably unlet b:winview afterwards.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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