13

I have a problem I can think of two general approaches for solving, but I don't know specifics for either approach.

...
Level 1:    cũng    also
Level 1:    và      and
Level 1:    như     like; such as
Level 2:    các     plural marker
Level 2:    của     belonging to
...

For each line starting "Level n" I want to insert a number, beginning with "01". For simplicity let's prepend the number.

Approach 1: Manually select all the lines with same Level. Invoke magic I am soon to learn.

Approach 2: Write a search and replace which matches all lines with given Level which at each match includes a number in the replace text, which increments by one with each match.

I've found similar questions on StackOverflow or on other Vim sites, but each seems to have one or more of the following problems:

  1. Is about inserting the current line number rather than an arbitrary but incrementing number.
  2. Doesn't zero-pad the number.
  3. Doesn't actually work for selections on my Vim 7.4 running on Windows 7. (These ones result in the error E481: No range allowed.)

I am running gVim in Windows with mswin.vim but a solution that works on all vanilla Vim installs without having to customize the setup might be best.

2
  • The best tags I could think of for the question do not exist: selection and range so feel free to re-tag or create one of those tags. Apr 29, 2015 at 1:33
  • 1
    This plugin is not a complete solution for your problem, but it's tremendously useful for adding columns of numbers: VisIncr. Docs here. FWIW.
    – lcd047
    Apr 30, 2015 at 6:18

3 Answers 3

21

Similar to the answer at https://vi.stackexchange.com/a/818/227, you can use the global command.

With it you can instruct vim to search for lines matching a pattern, and then perform commands on it.

In your case, you wish to prepend text to lines starting with "Level N:", so our global command could be

:g/^Level \d:/{COMMANDS}

Using the substitute command (regular expression replacement) for the command

The commands is more fun. I usually like to do a regular expression replacement for stuff like this, since it is easy to use variables.

Example for your question

:let i = 1 | g/^Level \d:/s/^/\=printf("%02d ", i)/ | let i = i+1

How it works

In the replacement section of a substitution command can be an expression.

The first thing we'll do is set a variable i to be the starting number. I chose 1, but any number will do. let i = 1

Then we run our global command, which sets us up to do an action on matched lines. g/^Level \d:/

We'll have our global command insert the value and increment our counter using the substitution command and the let command. s/^/\=printf("%02d ", i)/ | let i = i+1

The substitution command's regular expression finds the beginning of the line ^ and replaces it with an expression, and our expression will be the result of a formatted print. Like in the C language, vim's printf takes formatting parameters. %02d means convert an argument as though it was a decimal number d, occupying at least 2 spaces 2 and pad with 0 0. For details and other conversion options (including floating point formatting), see :help printf. We give printf our counting variable i and it gives us 01 the first time, 02 the second time, etc. This gets used by the substitution command to replace the beginning of the line, effectively inserting the result of the printf at the beginning.

Note that I put a space after the d: "%02d ". You didn't ask for it in the question (and I didn't see example output), but I suspected you wanted to separate the number from the word "Level". Remove the space from the string given to printf to have the inserted number right next to the L in Level.

Finally, that let i = i + 1 increments our counter after each substitution.

This can be applied generally for replacing parts of lines that are matched by other criteria with arbitrary functional data.

Using combined normal commands

This is good for simple insertions or complex editing. Like with substitute, we'll use global to match, but instead of regular expression substitution, we'll execute a series of operations as if typed by the user.

Example for your question

:let i = 1 | g/^Level \d:/execute "normal! I" . printf("%02d ", i) | let i = i+1

How it works

The values used are very similar to the substitute (we're still using printf to format our number to make it 0 padded with 2 digits), but the operation is different.

Here we use the execute command, which takes a string and runs the string as an ex command (:help :exe). We construct a string that combines "normal! I" with our data, which will be "normal! I01 " the first time and "normal! I02 " the second time, etc.

The normal command performs operations as if in normal mode. In this example, our normal command is I, which inserts at the beginning of the line. If we had used dd it would delete the line, o would open a new line after the matched line. It is as if you typed I (or any other operations) yourself in normal mode. we use the ! after normal to make sure no mappings get in our way. See :help :normal.

What is inserted then is the value of our printf, as in the first example of using substitute.

This method can be fancier than regex, because you can do things like execute "normal! ^2wy" . i . "th$p", which will go to the beginning of the text ^, move forward 2 words 2w, yank until the ith 'h' character y" . i . "th, move to the end of the line $, and paste p.

This is almost like running a macro, but doesn't actually use up a register and can combine strings from any expressions. I find this to be very powerful.

Approach where each level has its own counter

You might want each level to get its own counter. If you know the maximum number of levels ahead of time, you can do the following (adding extra code to find the largest level might not be too difficult, but would make this answer too long. This is getting long as it is).

First, lets free i, in case we already used it as an integer. We can't convert i to a list, we have to create it that way.

:unlet! i

Next, lets set i to be a list containing the number of levels. You showed 2 in your question, but lets assume 10 for the fun of it. Since list indexing is 0 based, and I don't want to bother correcting for 1 based like your list, we'll just create enough elements (11) and never use the 0 index.

:let j = 0
:let i = []
:while j < 11 | let i += [1] | let j += 1 | endwhile

Next, we need a way to get the level number. Fortunately, substitute is available as a function as well, so we'll give it our line and extract the level number substitute(getline("."), "^Level \\(\\d\\):.*", "\\=submatch(1)", "")

Since i is now a list of 11 1s (each index is the counter for our level), we can now adjust either of the above examples to use the result of this substitution:

Via substitute command:

:unlet! i | unlet! j | let j = 0 | let i = [] | while j < 11 | let i += [1] | let j += 1 | endwhile
:g/^Level \d:/let ind=str2nr(substitute(getline("."), "^Level \\(\\d\\):.*", "\\=submatch(1)", "")) | s/^/\=printf("%02d ", i[ind])/ | let i[ind] += 1

Via normal command:

:unlet! i | unlet! j | let j = 0 | let i = [] | while j < 11 | let i += [1] | let j += 1 | endwhile
:g/^Level \d:/let ind=str2nr(substitute(getline("."), "^Level \\(\\d\\):.*", "\\=submatch(1)", "")) | execute "normal! I" . printf("%02d ", i[ind]) | let i[ind] += 1

Example input:

Level 1: stuff

Level 1: Stuff

Some text
Level 3: Other

Level 1: Meh

Level 2: More

Example output:

01 Level 1: stuff

02 Level 1: Stuff

Some text
01 Level 3: Other

03 Level 1: Meh

01 Level 2: More
1
  • Wow, very encyclopedic. I've only read the first part and feel like I've learned more about Vim just from that than I have in the past few years. This is the kind of answer that makes Stack Exchange better than average Q&A sites and also shows the benefits of having a Vim specific SE. Apr 30, 2015 at 3:48
4

You could approach it by recording a macro. Starting with your original file, prepend the first instance with the number.

01 Level 1:    cũng    also
Level 1:    và      and
Level 1:    như     like; such as
Level 2:    các     plural marker
Level 2:    của     belonging to

Move your cursor to 01, select and yank it with yiw. Now you want to record your actions from this point out.

qq/^Level 1<CR>P<C-A>A<space><esc>0yiwq

  • qq Start a macro in register q
  • /^Level 1<CR> Search for a line starting with "Level 1"
  • P paste before cursor (contains your number)
  • <C-A> increments the number
  • A<space><esc> Insert a space after the number
  • 0 Move to the start
  • yiw yank the current number
  • q End the macro

Then repeat this macro by using @q.

8
  • 1
    Is <C-A> control+a? That selects everything in Vim on Windows. Apr 29, 2015 at 2:43
  • It is Control+a. In vim it should increment a number. If you have changed your keybindings to do Windows actions instead of Vim actions then your not going to be able to do most of the things people post here.
    – jecxjo
    Apr 29, 2015 at 2:45
  • No the Vim version for Windows comes with different bindings due to somebody's infinite wisdom. I'm just using the vanilla out-of-the-box bindings. I've never changed a Vim key binding in over twenty years, that I can recall (-: Apr 29, 2015 at 2:47
  • Shouldn't be the case, my Windows install has normal vim bindings. Could it be possible you installed someone else's build of vim? Or could you be running vim in "Easy" mode? i believe windows installs desktop icons with normal and easy mode options.
    – jecxjo
    Apr 29, 2015 at 2:50
  • 3
    No, its because the default install in windows loads mswin.vim. If you make your own vimrc and not load mswin.vim then you get the normal vim bindings. I keep a single vimrc for all my installs (Linux, Mac and Windows) and never deal with mswin.vim. For more info on this issue see stackoverflow.com/questions/289681/…
    – jecxjo
    Apr 29, 2015 at 2:53
3

You can build from https://stackoverflow.com/a/4224454/15934 to zero pad your numbers.

" A reminder: how to start numbers at the first line
:'<,'>s/^\s*\zs/\=(line('.') - line("'<")+1).'. '

But in order to simplify the action of padding numbers, I'd go for a pair of functions and a command:

command!  -range=% -nargs=? PrependNumbers <line1>,<line2>call s:Prepend(<args>)

function! s:ReplExpr(nb_digits, number)
  return repeat('0', a:nb_digits - strlen(a:number)).a:number
endfunction

function! s:Prepend(...) range
  let pattern = a:0 > 0 ? '\ze'. a:1 : '^'
  let nb_values = (a:lastline - a:firstline) + 1
  let nb_digits = strlen(nb_values)
  exe ':'.a:firstline.','a:lastline.'s#'.pattern.'#\=s:ReplExpr(nb_digits, 1+ line(".")-'.a:firstline.')." "#'
endfunction

Than, select your lines and type :PrependNumber (you'll see :'<,'>PrependNumber. [Note: The command takes an optional parameter: the pattern before which the number will be inserted]

7
  • But doesn't line(".") mean "use the current line number"? That was my problem 1. with previous answers I could find. Apr 29, 2015 at 8:19
  • 1
    Yes it is. That's why I subtract the line number of the first line in the range. Apr 29, 2015 at 8:20
  • Ah OK I haven't tested it yet because I can't even remember how to enter a script in Vim ... gotta read up on that first (-: Apr 29, 2015 at 8:23
  • 1
    As you're under windows, copy paste the code into $HOME/vimfiles/plugin/whatevernameyouwish.vim. Or even your $HOME/_vimrc (windows filename) if your prefer (in a first time). If you're not sure what $HOME is on your machine, ask vim what it thinks it is -> :echo $HOME Apr 29, 2015 at 8:27
  • 1
    You don't even need :source if the vim script is correctly installed in the right directory ({rtp}/plugin, or {rtp}/ftplugin/{filetype}/ftplugin for filetype specific plugins). :source is what we had to play with more than a decade and a half ago. Apr 29, 2015 at 8:35

Your Answer

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

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