2

I want to write a zsh vi-mode helper to wrangle complex commands but ran into a snag using \r in a substitute() function invocation. It results in \r replaced with control characters ^M in output. This works for command mode invocations, both mapping and creating a custom user command.

I've tried using single/double quotes for the {sub} string and surrounding the function declaration with set cpo+=C and set cpo-=C and replacing
\r with ctrl+v ctrl+m (to get ^M) but no joy.

The snippet below works for <leader>s, :Split but not for :Test

" .../after/ftplugin/zsh.vim
"
setlocal ts=2 sw=2 et sts=2 ai si

" For zsh vi-mode command split
" splits a single line command on every -- or -,
" appends \ on every eol
" mnemonic: cs = command split

" mapping shortcut to command works
" 1: Create a map, ask for confirmation
nmap <leader>s  :s:\(\s\{1,}\)\(-\+\): \\\r  \2:g<CR>  | noh | retab
nmap <leader>sq :s:\(\s\{1,}\)\(-\+\): \\\r  \2:cg<CR> | noh | retab

" user command works 
" 2: Create a User Command
command! -nargs=0 Split  s:\(\s\{1,}\)\(-\+\): \\\r\t\t\2:g  | noh | retab
command! -nargs=0 Splitq s:\(\s\{1,}\)\(-\+\): \\\r\t\t\2:cg | noh | retab

" 3: VimScript function doesn't work
" Fails with: ls -l -a
" EXPECT:
" ls\
"   -l\
"   -a
"
" GOT    : ls\^M -l\^M -a

command! -nargs=0 Test call SplitCommandLine()

func! SplitCommandLine()
  let pat    = '\(\s\{1,}\)\(-\+\)'
  let sub    = '\\\r \2'
  let flags  = 'g'
  let line   = getline('.')
  call setline('.', substitute(line, pat, sub, flags))
endf
2
  • Welcome to Vi and Vim @amal and thanks for the interesting and very well formulated question!
    – filbranden
    Commented Apr 28, 2020 at 15:21
  • 1
    Thanks @filbranden! happy to be here.
    – amal
    Commented Apr 28, 2020 at 18:11

1 Answer 1

1

The problem is not really with substitute(), but with setline(), which will not break lines on ^M and won't really offer a direct way to insert new lines at the place where the current line is being replaced.

You can pass setline() a list (which you can easily get by split()ing on "\r"), but that will actually replace the following lines, rather than inserting new lines for the continuation.

So, in order to make this work, you need to use setline() for the current line, followed by append() for the following lines.

The code below will do so:

func! SplitCommandLine()
  let pat    = '\(\s\{1,}\)\(-\+\)'
  let sub    = '\\\r \2'
  let flags  = 'g'
  let line   = getline('.')
  let repl   = split(substitute(line, pat, sub, flags), "\r")
  call setline('.', repl[0])
  call append('.', repl[1:])
endfunction

But arguably the best solution here is to stick to the :s Ex command (which you can easily call from a function), which already does what you need, without you having to jump through hoops to make it do it...

1
  • 1
    Oh neat! Interesting setline IO nursing effects the flow which highlights how flexible :s is for this task. Thanks @filbranden
    – amal
    Commented Apr 28, 2020 at 18:20

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.