Here is an attempt to write a function which will remove trashy characters (^F, ^S, ^Z, zero-width space, etc) on save, and keep cursor position.

colorscheme desert
set guifont=Consolas:h10
set encoding=utf-8
setglobal fileencoding=utf-8
set noexpandtab
set list
set listchars=tab:→\ ,space:·

set nobackup
set noswapfile
set noundofile

function! RemoveTrash()
  let l:save = winsaveview()
  keeppatterns %s/[^[:print:]\t]//g
  call winrestview(l:save)

augroup Test
  autocmd BufWritePre * :call RemoveTrash()
augroup END

The overall idea about function was taken from Martin Tournoij's answer about removing whitespace: https://vi.stackexchange.com/a/456, and regex was taken lincz's answer: https://stackoverflow.com/a/16135425.

Here is the test file:


To post it here I replaced tab characters with >--- and control characters (^F, ^S) with ##.

However, when I save the file, the cursor position isn't really saved - it slightly moves to the right side accordingly number of control characters before it. Here are two examples:

  • If you have cursor on letter a in bar - it will be moved to r.
  • If you have cursor on letter b in baz - it will be moved to z (because there are two control characters before it).

And here are screenshots for the first example:

enter image description here

How to fix it?

DJMcMayhem posted an answer at February the 5th, but it seems it doesn't properly work for tab-indented lines. That's why I started a bounty. I tried to fix it by changing getcurpos()[2] to getcurpos()[4], but my "knowledge" of Vimscript is too low.

  • 1
    The cursor position is correctly saved, just the text moved to the left, as you deleted characters.
    – Ralf
    Feb 5, 2019 at 18:35

1 Answer 1


Like Ralf said, your function is correctly saving the cursor position. The issue is when characters to the left of your cursor get deleted.

Here's what I came up with:

function! RemoveTrash()
  let l:initialCol = getcurpos()[2]
  let l:line = getline('.')[0:initialCol-1]

  let l:numRemoved = len(l:line) - len(substitute(l:line, '[^[:print:]\t]', "", "g"))

  let l:save = winsaveview()
  keeppatterns %s/[^[:print:]\t]//ge
  call winrestview(l:save)

  if l:numRemoved != 0
    call cursor(line('.'), (l:initialCol-l:numRemoved))

It keeps track of the old cursor position, and then calculates how many characters before that column will be deleted, and compensates.

Edit: switched exec normal | to cursor(), since that function compensates for tabs whereas | does not.

Edit 2: Added the /e flag to the substitute command so that it will not throw an error if there are no unprintables to remove.

  • It works, thanks. (I just want to wait 15-20 minutes before marked as "answered"). As I understand, you intentionally use two different types of quote signs (single and double)?
    – john c. j.
    Feb 5, 2019 at 18:58
  • @johncj Yes. I generally default to double quotes because I like them more, but I had some issues when I tried using them with substitute. I don't remember exactly, but I know that vim treats backslashes differently in single vs double quotes
    – DJMcMayhem
    Feb 5, 2019 at 19:02
  • Just in case, Google suggest to use single quoted strings: google.github.io/styleguide/…
    – john c. j.
    Feb 5, 2019 at 20:07
  • Also, echo l:numberOfCharsRemoved oftenly shows "0" for some reason (though removing itself works correctly).
    – john c. j.
    Feb 5, 2019 at 20:33
  • Oh yes, that echo shouldn't be there, I just forgot to remove it. If it's 0, that just means there won't be any unprintables on the current line, so nothing to worry about.
    – DJMcMayhem
    Feb 5, 2019 at 20:49

Your Answer

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

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