1

Libraries evolve all the time and I often face the situation where after an update some function name have changed and they return errors in the updated version of the library.

It's easy to replace all the instances of a word by another however I would like to keep track of the old version by commenting the line containing the old function name, as follows:

original line (which raises an error)

a line with old code

after:

// a line with old code
a line with new code

So far I did it manually: yy + p to copy the line, :%s/\<old code\>/new code/gc to replace with confirmation, then in the line I want to comment, esc + I + // + esc and I use the repeat command . to add my comment to the other appropriate lines. This is fine for few dozen lines of code but not for hundreds of lines, so I'd like to make this less manual.

The steps to be taken would be:

  • search lines with the old code (old function name)
  • copy the lines
  • comment the original lines: add // (or other) in front of the line
  • replace the old code in the uncommented line

Can this be done in a "vim-apprentice" friendly manner? More advanced methods will be welcome too.

  • Honestly, I like the question and i like the answers, it is all really interesting in the vim context. But in my opinion you don't need vim knowledge here but a version control system. Why keeping the old function names? It only bloats your code. I don't want to teach you here, i just want to give you a little thought-provoking impulse, and I am really interested if you have good reasons. – Doktor OSwaldo Jul 18 '17 at 8:14
  • @DoktorOSwaldo Good and legitimate remark! My use case is when I want to test some code snippets downloaded from blogs, github or other places. For such simple cases, what version control system would you suggest? For large projects of my own I would use git, which I'm trying to get familiar with. I keep the old function name to keep track of what went wrong, and in case I run into some similar errors in the future, I have one file to open and see what caused problem and the solution I've found. – calocedrus Jul 18 '17 at 8:30
  • I would use git for everything. True it does take time to get familiar, I mean I do not completly understand git, I would even say I am rather bad with git. But git is in my opinion just the best version control out there! And most the projects around vim etc are on git (And there are awesome git plugins). I f.e. use vim-fugitive which allows me to cycle through all old versions of the file i have open inside my vim. With git you can lookup every step the file has taken and so on. But as I said, i am not the right guy for tips on version control, but there are surely enough articels about it – Doktor OSwaldo Jul 18 '17 at 8:48
2

Script

func! CoRep (pat, ...)
    let line_loc    = search (a:pat, 'nw')
    let line        = getline (line_loc)

    call setline (line_loc, '// '.line)

    let pair = 0
    while pair < a:0
        let pat = a:000[pair]
        let sub = a:000[pair+1]

        let line = substitute (line, pat, sub, '')
        let pair = pair + 2
    endwhile

    call append (line_loc, line)
endf

The arguments are:

  1. First argument is a line pattern. Line is being searched down the cursor, wrapping to the top.
  2. Other arguments are pairs of pattern and substitution strings (up to 9 pairs). Substitutions are applied to the entire line, in the same order, and stop at the first match.

Usage

Call from the command mode or map to leader-r (or any other key).

nnoremap <leader>r :CoRep 
command! -nargs=+ CoRep : call CoRep (<f-args>)

demo

Update

Applies substitution to all matched lines at once

func! CoRepGlob (pat, ...)
    let origin = getcurpos()
    call cursor (1, 0)

    while 1
        let line_loc = search (a:pat, 'cW')
        if line_loc == 0
            break
        endif

        let line = getline (line_loc)
        call setline (line_loc, '// '.line)

        let pair = 0
        while pair < a:0
            let pat = a:000[pair]
            let sub = a:000[pair+1]

            let line = substitute (line, pat, sub, '')
            let pair = pair + 2
        endwhile

        call append (line_loc, line)
        call cursor (line_loc + 2, 0)
    endwhile

    call setpos ('.', origin)
endf
1

You can use capture groups in your replace and replace the original line with two lines. Here is an example with python changing print statements to have parentheses (python 2 --> python 3)

Original text:

print 'woo'

print 'woosh'

print 'wee'

print 'weesh'

Command:

:%s/print \(.*\)/#print \1 \rprint(\1)/g

Result:

#print 'woo' 
print('woo')

#print 'woosh' 
print('woosh')

#print 'wee' 
print('wee')

#print 'weesh' 
print('weesh')

If the command you are trying to replace is long this may be a poor solution

  • 1
    \0 is the whole match, so this is a shorter/simpler version of your technique: %s/print \(.*\)/#\0\rprint(\1)/g – Rich Jul 18 '17 at 12:59
  • Even shorter: %s/.*/#&\r&/ – Christian Brabandt Jul 18 '17 at 17:42
  • @ChristianBrabandt this looks promising, but you still need to have a capture group since you will want only part of the match. Correct me if I've wrong, but I think your answer is only shorter than @Rich because of & instead of \0 – mitoRibo Jul 18 '17 at 18:27
  • from the example, it is not clear what exactly you want. – Christian Brabandt Jul 18 '17 at 19:43
1

This is a very rough function, that should do what you want:

    " <range>CopyAndComment char/line/match/replace
    com! -range=% -nargs=+ CopyAndComment let wsv=winsaveview()|<line1>,<line2>call s:CopyAndComment(<q-args>)|call winrestview(wsv)

    function! s:CopyAndComment(args) range
        let arg=split(a:args, '/')
        if len(arg) > 4 || len(arg) < 3
            echoerr "Usage: <range>CopyAndComment char/line/match/replace"
            return
        endif
        if len(arg) == 3
            let com = split(getbufvar("%", '&cms', '# %s'), '%s')[0]
            call insert(arg, com, 0)
        endif
        let arg=map(arg, 'escape(v:val, "/#")')
        let s_replace_arg = '\="'.arg[0]. '".submatch(0).'.'"\n"'.'.substitute(submatch(0),"'.arg[2].'","'.arg[3].'","g")'
        exe a:firstline.','.a:lastline.'g/'.arg[1].'/s#.*#'.s_replace_arg.'#g'
    endfunc

This defines a command :CopyAndComment that takes one argument of the form comment/linematch/search/replace and it will use the linematch regex to find matching lines to copy. In each matching line, it will copy the line, add a comment char to the original line, and replace the newly created line according to the search/replace argument. You can leave out the firsts comment char, in which case the function will take the value from the &commentstring option and use that (only the first half of it) which defaults to /*.

Additionally, the command takes a range argument, so if you only want to replace in the lines 1-5, you would call it like 1,5CopyAndComment \#/line/foo/bar. If no range is given, it defaults to the whole file.

Internally the function works by sticking together a :g command with a :s command doing all those replacements. However quoting is really ugly, it took my a couple of minutes to twist my brain until I got that correct.

For your example you would then call it as :CopyAndComment \/\/ /line/old/new (note you have to escape the comment chars // as this is used as delimiter)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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