Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Vim Scripting - call function once for all selected lines

Tags:

vim

So I have a text file like this:

Item a: <total>
  Subitem: 10 min
  Subitem 2: 20 min

I'd like to replace <total> with the total of 10 and 20. Right now I'm doing it with the following functions:

let g:S = 0  "result in global variable S
function! Sum(number)
  let g:S = g:S + a:number
  return a:number
endfunction

function! SumSelection()
  let g:S=0
  '<,'>s/\d\+/\=Sum(submatch(0))/g
  echo g:S
endfunction

vnoremap <s-e> call SumSelection()<cr>

Sum gets the sum of numbers passed in, SumSelection calls sum over all the numbers in selected lines, and (supposedly) Shift+e calls SumSelection in visual mode (or whatever you choose to call it.)

Problem is, when I hit Shift+e when I have some lines selected, instead of :call SumSelection() I really get :'<,'>call SumSelection() which means the function gets called once per selected line. No good, right? So as far as I can tell there's no way around this. What can I do to get the function to:

  1. be only called once
  2. maybe total these in a more efficient way
like image 670
Brian Hicks Avatar asked Dec 21 '11 22:12

Brian Hicks


1 Answers

Well, to have a function execute just once over a range, append the range keyword. E.g.

fun Foo() range
    ...
endfun

Then your function can take care of the range itself with the special parameters a:firstline and a:lastline. (See :help a:firstline for details.)

However I think that in this case your requirements are just about simple enough to be accomplished with a one-liner using :global.

We could do with better specified inputs and outputs really. But assuming a file containing

Item a: <total>
  Subitem: 10 min
  Subitem 2: 20 min
Item b: <total>
  Subitem: 10 min
  Subitem 2: 23 min
Item c: <total>
  Subitem: 10 min
  Subitem 2: 23 min
  Subitem 3: 43 min

and a function defined as

fun! Sum(n)
    let g:s += a:n
    return a:n
endfun

then this will sum the subitems (all one line)

:g/^Item/ let g:s = 0 | mark a | +,-/\nItem\|\%$/ s/\v(\d+)\ze\s+min$/\=Sum(submatch(0))/g | 'a s/<total>/\=(g:s . ' min')/

and produce this output

Item a: 30 min
  Subitem: 10 min
  Subitem 2: 20 min
Item b: 33 min
  Subitem: 10 min
  Subitem 2: 23 min
Item c: 76 min
  Subitem: 10 min
  Subitem 2: 23 min
  Subitem 3: 43 min

By the way, the above command acts on the whole buffer. If you highlight a range first and then hit the : key, vim will automatically prepend your command with '<,'> meaning that the following command will operate from the start to the end of the highlighted range.

E.g. highlighting lines 1 to 5 and then running the command (:'<,'> g/...) produces this output

Item a: 30 min
  Subitem: 10 min
  Subitem 2: 20 min
Item b: 33 min
  Subitem: 10 min
  Subitem 2: 23 min
Item c: <total>
  Subitem: 10 min
  Subitem 2: 23 min
  Subitem 3: 43 min

One final note. If one of the groups has no subitems, e.g.

Item a: <total>
  Subitem: 10 min
  Subitem 2: 20 min
Item b: <total>
Item c: <total>
  Subitem: 10 min
  Subitem 2: 23 min
  Subitem 3: 43 min

then the command will abort with an 'Invalid range' error when it reaches the second item. You can get Vim to ignore this and carry on regardless by prefixing the whole command with :silent!.

EDIT

Just a quick explanation of my workflow and why the long one-liner.

  1. I like :g and ex commands a lot.
  2. I built up the command interactively using the command-line history and the command-line window for editing.
  3. When I'm done I paste one-off commands like this into the buffer: I have a mapping to execute the current line as an ex command. That way the command is always easily available when I need it. For more regular use you might want a command, function, or a ftplugin.
like image 134
1983 Avatar answered Sep 17 '22 11:09

1983