17

Imagine I have the following text:

some random stuff
* asdf
* foo
* bar
some other random stuff

I want to replace the asterisk bullets with numbers, like so:

some random stuff
1. asdf
2. foo
3. bar
some other random stuff

How can this be done in vim ?

2
  • Why don't you go for plugins? A similar one is increment.vim in Github
    – SibiCoder
    Apr 16, 2016 at 11:01
  • It is so amazing and cool that everybody made their answers increment the numbers but since Markdown will number them for you why not just make them all 1.? So :%s/^* /1. / would do it. That seems like much less work.
    – chicks
    Aug 1, 2017 at 1:41

5 Answers 5

15

You could try the following command:

:let c=0 | g/^* /let c+=1 | s//\=c.'. '

First it initializes the variable c (let c=0), then it executes the global command g which looks for the pattern ^* (a beginning of line, followed by an asterisk and a space).

Whenever a line containing this pattern is found, the global command executes the command:
let c+=1 | s//\=c.'. '
It increments the variable c (let c+=1), then (|) it substitutes (s) the previous searched pattern (//) with the evaluation of an expression (\=):
the contents of variable c concatenated (.) with the string '. '


If you don't want to modify all the lines from your buffer, but only a specific paragraph, you can pass a range to the global command. For example, to modify only the lines whose number is between 5 and 10:

:let c=0 | 5,10g/^* /let c+=1 | s//\=c.'. '

If you have a file containing several similar lists which you want to convert, for example something like this:

some random stuff                 some random stuff                      
* foo                             1. foo                                 
* bar                             2. bar                                 
* baz                             3. baz                                 
some other random stuff           some other random stuff                
                           ==>                                                
some random stuff                 some random stuff                      
* foo                             1. foo                                 
* bar                             2. bar                                 
* baz                             3. baz                                 
* qux                             4. qux                                 
some other random stuff           some other random stuff                

You can do it with the following command:

:let [c,d]=[0,0] | g/^* /let [c,d]=[line('.')==d+1 ? c+1 : 1, line('.')] | s//\=c.'. '

It's just a variant of the previous command, which resets the variable c when you switch to another list. To detect whether you are in another list, the variable d is used to store the number of the last line where a substitution was made.
The global command compares the current line number (line('.')) with d+1. If they are the same, it means we are in the same list as before so c is incremented (c+1), otherwise it means we are in a different list, so c is reset (1).

Inside a function, the command let [c,d]=[line('.')==d+1 ? c+1 : 1, line('.')] could be rewritten like this:

let c = line('.') == d+1 ? c+1 : 1
let d = line('.')

Or like this:

if line('.') == d+1
    let c = c+1
else
    let c = 1
endif
let d = line('.')

To save some keystrokes, you could also define the custom command :NumberedLists, which accepts a range whose default value is 1,$ (-range=%):

command! -range=% NumberedLists let [c,d]=[0,0] | <line1>,<line2>g/^* /let [c,d]=[line('.')==d+1 ? c+1 : 1, line('.')] | s//\=c.'. '

When :NumberedLists will be executed, <line1> and <line2> will be automatically replaced with the range you used.

So, to convert all the lists in the buffer, you would type: :NumberedLists

Only the lists between line 10 and 20: :10,20NumberedLists

Only the visual selection: :'<,'>NumberedLists


For more information, see:

:help :range
:help :global
:help :substitute
:help sub-replace-expression
:help list-identity    (section list unpack)
:help expr1
:help :command
9

This only works with a recent Vim version (that has :h v_g_CTRL-A):

  1. Block-select the list bullets (*) and replace them with 0 (cursor is on first *): Ctrl-v j j r 0.
  2. Reselect previous block and increment with counter: gv g Ctrl-a

... and that's it :)


(If you want to have a dot after each number, change 1st step to: Ctrl-v j j s 0 . Esc)

9

Visually select the lines and execute this substitution command:

:'<,'>s/*/\=line('.') - line("'<") + 1 . '.'

See :help sub-replace-expression, :help line(), and :help '<.

To avoid having to select the lines, backward and forward searches with offsets can be used to specify the substitution range like this:

:?^[^*]?+1,/^[^*]/-1s/*/\=line('.') - search('^[^[:digit:]]', 'bn') . '.'

See :help cmdline-ranges

2

Another way:

:let n = 1 | g/^* /s//\=printf('%d. ', n)/g | let n = n + 1
0

You could also define custom operators

You could map them to the key sequences '* and '#. The marks * and # don't exist, so you won't override any default functionality. The reason to choose ' as a prefix is to get some kind of mnemonics. You're adding a sign/mark in front of some lines. And usually to go to a mark you use the prefix '.

fu! s:op_list_bullet(...) abort range

    if a:0
        let [lnum1, lnum2] = [line("'["), line("']")]
    else
        let [lnum1, lnum2] = [line("'<"), line("'>")]
    endif

    if !empty(matchstr(getline(lnum1), '^\s*\d\s*\.'))
        let pattern     = '\d\s*\.\s\?'
        let replacement = '* '

    elseif count(['-', '*'], matchstr(getline(lnum1), '\S'))
        let pattern     = '\v\S\s*'
        let replacement = ''

    else
        let pattern     = '\v\ze\S'
        let replacement = '* '
    endif

    let cmd = 'keepj keepp %s,%s s/%s/%s'

    sil exe printf(cmd, lnum1, lnum2, pattern, replacement)
endfu

fu! s:op_list_digit(...) abort range
    let l:c = 0

    if a:0
        let [lnum1, lnum2] = [line("'["), line("']")]
    else
        let [lnum1, lnum2] = [a:firstline, a:lastline]
    endif

    if count(['-', '*'], matchstr(getline(lnum1), '\S'))
        let pattern     = '\S\s*'
        let replacement = '\=l:c.". "'

    elseif !empty(matchstr(getline(lnum1), '^\s*\d\s*\.'))
        let pattern     = '\d\s*\.\s\?'
        let replacement = ''

    else
        let pattern     = '\v^\s*\zs\ze\S'
        let replacement = '\=l:c.". "'
    endif

    let cmd = 'keepj keepp %s,%s g/%s/let l:c = line(".") == line("'']")+1 ?
                                                \ l:c+1 : 1 |
                                                \ keepj keepp s/%s/%s'

    sil exe printf(cmd, lnum1, lnum2, pattern, pattern, replacement)
endfu

nno <silent> '*     :<C-U>set opfunc=<SID>op_list_bullet<CR>g@
nno <silent> '**    :<C-U>set opfunc=<SID>op_list_bullet
                    \<Bar>exe 'norm! ' . v:count1 . 'g@_'<CR>
xno <silent> '*     :call <SID>op_list_bullet()<CR>

nno <silent> '#     :<C-U>set opfunc=<SID>op_list_digit<CR>g@
nno <silent> '##    :<C-U>set opfunc=<SID>op_list_digit
                    \<Bar>exe 'norm! ' . v:count1 . 'g@_'<CR>
xno <silent> '#     :call <SID>op_list_digit()<CR>

It also works from visual mode.
Ex commands are good for scripting, but for an interactive use, a normal operator is probably better, because you can combine it with any motion or text-object.

For example, you could toggle a list prefixed with asterisks or minus signs inside the current paragraph by hitting '*ip. Here, '* is an operator and ip is the text-object on which it works.

And do the same thing for a list prefixed with numbers across the next 10 lines by hitting '#10j. Here, '# is another operator and 10j is a motion covering the lines on which the operator works.

The other benefit of using a custom operator, is that you can repeat your last edition with the dot command.

enter image description here

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.