Fixing a function that receives commands as arguments

What is wrong with this function?

-- execute commands without changing cursor position
function preserve(cmd)
    local cmd = 'keepjumps keeppaterns ' .. cmd
    local original_cursor = vim.fn.winsaveview()
    vim.api.nvim_command(cmd)
    vim.fn.winrestview(original_cursor)
end

If I try

preserve('%s/\s\+$//e')

Neovim complains:
E5112: Error while creating lua chunk: test.lua:8: invalid escape sequence near ‘’%s/’

Could be due to the backslash escaping the character, it will try to assert what \s means, which doesn’t make sense if it’s not evaluated in a regex context. Not really valid syntax, or at least that’s my guess.

You want to preserve the backslash as well so it’s passed onto nvim_command properly. Try to escape those backslashes as well, or wrap the string in double square brakets ([[ ]]) if you don’t want to do the double backslashes.

-- Escape the backslahes
preserve('%s/\\s\\+$//ge')

-- OR use double square brackets
preserve([[%s/\s\+$//ge]])

I have tested both options with no avail.

Hmm looks like I am also getting this when I try it out, seems like it doesn’t like how the string is injected into the function. I tried it by binding to a command and got the same error as you did, so all I did was wrap the line where I was defining the function with the double square brackets.

However, there is one more error you might end up, which I got as a Trailing Character error, turns out you can’t just add an expression after calling commands, it’s best to execute the command you want after calling keepjumps and keeppatterns. Here is what I ended up doing:

function _G.preserve(cmd)
  cmd = string.format('keepjumps keeppatterns execute %q', cmd)
  local original_cursor = vim.fn.winsaveview()
  vim.api.nvim_command(cmd)
  vim.fn.winrestview(original_cursor)
end

vim.cmd([[command! Preserve lua preserve('%s/\\s\\+$//ge')]])

Only line I modified is the first line of the function, I’ve added execute %q, the %q in this case is used by string.format() which just adds quotes to the variable. So in that case the full cmd string should be: keepjumps keeppaterns execute "%s/\s\+$//ge"

Basically, you want to execute the last function, this can take any vim Ex command so it should be fine but idk your use case for this besides trying to execute the expression of %s.

1 Like

The actual idea for this is to wrap a bunch of solutions like:

  • Removing trailing spaces
  • Reindenting the code
  • Deleting blank lines

Each one of these functions would be wrapped up into the preserve function. Heres my solution in pure vim:

command! Cls :call Preserve(':%s/\v\s+$//e')
cnoreabbrev cls Cls
cnoreabbrev StripTrailingSpace Cls

" https://stackoverflow.com/questions/4545275/
command! BufOnly silent! call Preserve("exec '%bd|e#|bd#'")
cnoreab bo BufOnly

command! -nargs=0 Reindent :call Preserve('exec "normal! gg=G"')

" join lines keeping cursor position
nnoremap J :call Preserve(':join')<CR>
nnoremap <Leader>J :call Preserve(':join!')<CR>

if !exists('*ChangeHeader')
    " updates the file timestamp
    fun! ChangeHeader() abort
        if line('$')>=7
            keepj keepp call Preserve(':1,7s/\v(Last (Change|Modified)|date):\s+\zs.*/\=strftime("%b %d, %Y - %H:%M")/ei')
        endif
    endfun
endif

if !exists('*Dos2unixFunction')
    fun! Dos2unixFunction() abort
        "call Preserve('%s/ $//ge')
        call Preserve(":%s/\\%x0D$//e")
        set ff=unix
        set bomb
        set encoding=utf-8
        set fileencoding=utf-8
    endfun
endif
com! Dos2Unix :call Dos2unixFunction()
cnoreabbrev dos2unix Dos2unix
cnoreabbrev d2u Dos2Unix

The actual preserve uses a try catch

if !exists('*Preserve')
    function! Preserve(command)
        try
            let l:win_view = winsaveview()
            "silent! keepjumps keeppatterns execute a:command
            silent! execute 'keeppatterns keepjumps ' . a:command
        finally
            call winrestview(l:win_view)
        endtry
    endfunction
endif

I think many people can copy some of these ideas or improve them as well! Thanks!
I have also added “silent!” to the preserve function.

That’s probably because lua uses % for patterns, see Programming in Lua : 20.2. I assume it should be enough to replace % by %% in your code, but I can’t test right now.