4

Writing unit tests in RSpec, I frequently change something like this,

it 'eats pizza' do
  pizza = build(:pizza)
  expect(subject.eats?(pizza)).to be_true
end

to this,

let(:pizza) { build(:pizza) }

it 'eats pizza' do
  expect(subject.eats?(pizza)).to be_true
end

I want to automate part of this to easily (and repeatably) convert

name = value

to

let(:name) { value }

I've done it with a macro:

mzIlet(:<Esc>ea)<Esc>f=r{A }<Esc>`z

but I'd like something I can put in my vimrc. I came up with a command as well:

command! -bang Let execute "normal! mzIlet\<bang>(:\<Esc>ea)\<Esc>f=r{A }\<Esc>`z"

which is a little better, since I can run :Let to get let(:name) or :Let! to get let!(:name). But it's not repeatable with the dot command (though @: is a decent consolation in this case).

Going one step further, this searches backwards for the first let, context, or describe line and puts the let line below it:

command! -bang Let execute "normal! dd?\\v^%(\\s+let>)|%(\\s*%(context|describe)>)\<CR>p==Ilet\<bang>(:\<Esc>ea)\<Esc>f=r{A }\<Esc>`'"

Really feels like something that's outgrowing a command, and should be implemented as a function.

How can I make this repeatable? Is a command not the way to go with this? Looks like tpope's repeat.vim can make key-maps repeatable.

Update The implementation has changed, as the problem is a better fit for pattern matching & substitution than I originally realized. I haven't made it . repeatable yet, but that's the next step (and thanks to @saginaw for the advice below).

func! RSpecLet(bang)
  delete
  normal! mz

  call MoveAssignment()
  call ReplaceWithLet(a:bang)

  normal! `z
endf

func! MoveAssignment()
  call search('\v^\s*<let>|<context>|<describe>', 'b')
  put
  normal! ==
endf

func! ReplaceWithLet(bang)
  execute 'substitute/\v^\s+\zs'
        \ '(\w+)%(\s*\=\s*)(.*\S)\s*$/'
        \ 'let'.a:bang.'(:\1) { \2 }'
endf
command! -bang Let call RSpecLet('<bang>')
2

To make your first command repeatable with dot, you could try the following code:

function! s:RSpecLet(bang)
    if a:bang ==# '!'
        execute "normal! mzIlet!(:\eea)\ef=r{A }\e`z"
        call repeat#set("\<Plug>RSpecBangLet")
    else
        execute "normal! mzIlet(:\eea)\ef=r{A }\e`z"
        call repeat#set("\<Plug>RSpecLet")
    endif
endfunction

nnoremap <silent> <Plug>RSpecLet     :<C-U>call <SID>RSpecLet('')<CR>
nnoremap <silent> <Plug>RSpecBangLet :<C-U>call <SID>RSpecLet('!')<CR>

command! -bang Let call s:RSpecLet('<bang>')

Note that it calls the repeat#set() function provided by the repeat plugin from tpope, so you would need to install the latter to make the previous code work.

If you don't want to install a plugin, it can be done, but the code would be around 40 lines longer.

If you want the code to be a bit easier to understand, you could break down your editions into small chunks (each of them could be preceded by a comment if needed) like this:

function! s:RSpecLet(bang)
    normal! mz

    if a:bang ==# '!'
        normal! Ilet!(:
    else
        normal! Ilet(:
    endif

    normal! ea)
    normal! f=r{A }
    normal! `z

    if a:bang ==# '!'
        call repeat#set("\<Plug>RSpecBangLet")
    else
        call repeat#set("\<Plug>RSpecLet")
    endif
endfunction

nnoremap <silent> <Plug>RSpecLet     :<C-U>call <SID>RSpecLet('')<CR>
nnoremap <silent> <Plug>RSpecBangLet :<C-U>call <SID>RSpecLet('!')<CR>

command! -bang Let call s:RSpecLet('<bang>')

To make the 2nd command repeatable with dot, you could try the following code, similar to the previous one:

function! s:RSpecLet(bang)
    if a:bang ==# '!'
        execute "normal! dd?\\v^%(\\s+let>)|%(\\s*%(context|describe)>)\rp==Ilet!(:\eea)\ef=r{A }\e`'"
        call repeat#set("\<Plug>RSpecBangLet")
    else
        execute "normal! dd?\\v^%(\\s+let>)|%(\\s*%(context|describe)>)\rp==Ilet(:\eea)\ef=r{A }\e`'"
        call repeat#set("\<Plug>RSpecLet")
    endif
endfunction

nnoremap <silent> <Plug>RSpecLet     :<C-U>call <SID>RSpecLet('')<CR>
nnoremap <silent> <Plug>RSpecBangLet :<C-U>call <SID>RSpecLet('!')<CR>

command! -bang Let call s:RSpecLet('<bang>')

I've read your edit and I think you're right, your new implementation is probably better. If you want to make it repeatable, here's a possible solution:

func! RSpecLet(bang)
  delete
  normal! mz

  call MoveAssignment()
  call ReplaceWithLet(a:bang)

  execute 'call repeat#set("\<Plug>RSpec' . a:bang . 'Let")'
  normal! `z
endf

func! MoveAssignment()
  call search('\v^\s*<let>|<context>|<describe>', 'b')
  put
  normal! ==
endf

func! ReplaceWithLet(bang)
  execute 'substitute/\v^\s+\zs'
        \ '(\w+)%(\s*\=\s*)(.*\S)\s*$/'
        \ 'let'.a:bang.'(:\1) { \2 }'
endf

nnoremap <silent> <Plug>RSpecLet  :<C-U>call RSpecLet('')<CR>
nnoremap <silent> <Plug>RSpec!Let :<C-U>call RSpecLet('!')<CR>

command! -bang Let call RSpecLet('<bang>')

If you don't want to install the repeat.vim plugin, here's another possible solution (which copies the bare minimum from the original plugin):

func! RSpecLet(bang)
  delete
  normal! mz

  call MoveAssignment()
  call ReplaceWithLet(a:bang)

  execute 'call RepeatSet("\<Plug>RSpec' . a:bang . 'Let")'
  normal! `z
endf

func! MoveAssignment()
  call search('\v^\s*<let>|<context>|<describe>', 'b')
  put
  normal! ==
endf

func! ReplaceWithLet(bang)
  execute 'substitute/\v^\s+\zs'
        \ '(\w+)%(\s*\=\s*)(.*\S)\s*$/'
        \ 'let'.a:bang.'(:\1) { \2 }'
endf

nnoremap <silent> <Plug>RSpecLet  :<C-U>call RSpecLet('')<CR>
nnoremap <silent> <Plug>RSpec!Let :<C-U>call RSpecLet('!')<CR>

command! -bang Let call RSpecLet('<bang>')

func! RepeatSet(sequence,...)
    let g:repeat_sequence = a:sequence
    let g:repeat_count = a:0 ? a:1 : v:count
    let g:repeat_tick = b:changedtick
endf

func! RepeatDot(count)
    if g:repeat_tick == b:changedtick
      let c = g:repeat_count
      let cnt = (a:count ? a:count : (c ? c : ''))
      call feedkeys(g:repeat_sequence, 'i')
      call feedkeys(cnt, 'ni')
    else
      call feedkeys((a:count ? a:count : '') . '.', 'ni')
    endif
endf

func! RepeatWrap(command)
  let sync_test = (g:repeat_tick == b:changedtick)
  execute 'normal! ' . a:command . 'zv'
  if sync_test
    let g:repeat_tick = b:changedtick
  endif
endf

let g:repeat_tick = -1
augroup Repeat
  autocmd!
  autocmd BufLeave,BufWritePre,BufReadPre * let g:repeat_tick =
          \ (g:repeat_tick == b:changedtick || g:repeat_tick == 0) ? 0 : -1

  autocmd BufEnter,BufWritePost * if g:repeat_tick == 0 |
          \ let g:repeat_tick = b:changedtick |
          \ endif
augroup END

nmap .     <Plug>(RepeatDot)
nmap u     <Plug>(RepeatUndo)
nmap U     <Plug>(RepeatUndoLine)
nmap <C-R> <Plug>(RepeatRedo)

nnoremap <silent> <Plug>(RepeatDot)      :<C-U>call RepeatDot(v:count)<CR>
nnoremap <silent> <Plug>(RepeatUndo)     :<C-U>call RepeatWrap('u')<CR>
nnoremap <silent> <Plug>(RepeatUndoLine) :<C-U>call RepeatWrap('U')<CR>
nnoremap <silent> <Plug>(RepeatRedo)     :<C-U>call RepeatWrap("\<Lt>C-R>")<CR>
|improve this answer|||||
1

I have an alternate solution using a plugin I created. The plugin is called express.vim, and it helps with the creation of ad-hoc operators. With it, you can create two types of operators:

  • Operators that run a VimScript expression over the supplied motion
  • Operators that perform a substitution over the supplied motion

One thing I observed is that your edit could be performed using the :substitute command:

:s/\(.\{-}\)\s*=\s*\(.*\)/let(:\1) { \2 }/

The pattern looks for text like foo = bar, capturing the left and right hand sides of the = sign; the replacement inserts them into the RSpec let statement as you specified.

To create an operator that does this over a motion, we can use the :MapSubpress command. In the below example, we'll map it to cd.

:MapSubpress cd /\(.\{-}\)\s*=\s*\(.*\)/let(:\1) { \2 }/

Now cd is an operator that we can use and repeat with . (this also depends on repeat.vim). Note that since it's an operator, it requires a motion to follow (just as c or y would). Since you're usually operating on lines, the _ motion will suffice. So we could use cd_ to transform foo = bar into let(:foo) { bar }.

If you're always operating on a line, you might as well make a mapping that always invokes the _ motion. For this, I'd use a <Plug> mapping with :MapSubpress, and then create a map that refers to it. You could put something like this in your vimrc:

MapSubpress <Plug>(rspec-let) /\(.\{-}\)\s*=\s*\(.*\)/let(:\1) { \2 }/
nmap cd <Plug>(rspec-let)_

Now, our cd mapping will operate on whole lines every time, and it's still repeatable with ..

|improve this answer|||||

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.