Vim: multi-command for filtering out blocks of tex

2019-06-03 07:07发布

问题:

This is a follow-up to my question about using multi-line regex in Vim.

A quick explanation: I have blocks of text tagged with #tags and separated by a blank line. I want to filter out all blocks that include a specific tag. I was helped with regex here (thanks!), so I constructed the following command:

command -nargs=1 TagSearch g/^\(.\+\n\)\{-}.*#<args>.*/y a | vnew | put a | %s/^#.*<args>.*\n/&\r 

Hence, doing a :TagSearch tag should:

  1. Search for the #tag,
  2. paste all corresponding Text Blocks into a new vertical buffer,
  3. Add a blank line between the Text Blocks in the new buffer.

Q1: When I do each step separately, everything works. But the multi-command only pastes the first matching text block into the new buffer. Why?

Q2: How could I make the command accept multiple arguments (in case I want to search for many #tags at a time)? Thanks!

回答1:

One can use the following implementation.

command! -nargs=* -bar TagSearch call CollectParagraphs([<f-args>])
function! CollectParagraphs(tags) range
    let tags = len(a:tags) > 0 ? a:tags : [expand('<cword>')]
    let pats = map(copy(tags), '"\\.\\*#" . escape(v:val, "\\")')

    let v = winsaveview()
    let [sr, @/; lines] = [@/, '\V' . join(pats, '\&')]
    g//call extend(lines, getline(search('\n\n\zs', 'bnW'), line("'}")))
    let @/ = sr
    call winrestview(v)

    exe 'vnew' escape(join(tags), ' %#|\')
    set buftype=nofile bufhidden=hide noswapfile
    call setline(1, lines)
endfunction


回答2:

I had a good old play around with this and learnt quite a bit in the process! Looks like there are a few problems going on here.

Here's how I worked through to get it working, although as there are so many approaches with vim there is probably a tidier way somehow.

The g command is of the form g/pattern/command. What I think was happening was in your original form, the | vnew | put a | %s... part of the command was being executed linewise with the g command, rather than once as part of the TagSearch command. I changed the g command to being 'executed' instead which solves the problem - I'd be interested to know if there is a way to specify what the pipe applies to without using execute, I couldnt get it working (brackets don't work for example). This would have been why you were only getting the first line. This gives us (fixing a typo in your '%s' command):

command! -nargs=1 TagSearch execute 'g/^\(.\+\n\)\{-}.*#<args>.*/y a' | vnew | put a | %s/^.*#<args>.*\n/&\r

This seems to reverse the problem though, and we now only get the last line in the new buffer. The other problem with the g command, is that when you do /y a, it overwrites the a register each time. There is a way to get vim to append to a register instead, using the capitalized register name (/y A) (see :help quotea). Doing it this way we need to initialize the register to blank first, using let. That gives us this:

command! -nargs=1 TagSearch let @a='' | execute 'g/^\(.\+\n\)\{-}.*#<args>.*/y A' | vnew | put a | %s/^.*#<args>.*\n/&\r

Finally to get it to execture with multiple tags, I just fiddled with the <args> a bit (calling TagSearch tag1 tag2, args would literally be the string 'tag1 tag2') to get it to fit in the regex as below:

command! -nargs=* TagSearch let @a='' | execute 'g/^\(.\+\n\)\{-}.*#\(' . substitute('<args>', ' ', '\\|', 'g') . '\).*/y A' | vnew | put a | execute '%s/^.*#\(' . substitute('<args>', ' ', '\\|', 'g') . '\).*\n/&\r'

If you add any more features to this you might want to try playing around with a little vimscript function or something instead, otherwise it might get pretty hard to maintain! You'd be able to grab your text blocks into nice lists and potentially process them a bit more easily rather than having to do everything as if you were actually typing. Take a look at :help functions for things that are available (although there may be a better starting point in the help for vimscript somewhere else).