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
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 withw: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. Usingb:winview
solves that.