Vimで選択したテキストに対してcgnする

1 min read読了の目安(約1200字

カーソル下の単語を検索して、一度戻ってから次のマッチ(gn)を変更(c)する。
このcgnをドットリピートするテクニックがよく知られている。
以下のマッピングは一例。

nnoremap <Space>d *N"_cgn

📝 レジスタを汚さないために_に捨てている

これをより汎用的にして、ビジュアルモードで選択したテキストでcgnできるようにした。

xnoremap <expr> <Space>d "\<ESC>/\<C-r>=<SID>search()\<CR>\<CR>N\"_cgn"
function! s:search() abort
  " 選択範囲のテキストを正確に取るのが面倒で元の内容を一時退避してyしている
  let tmp = @"
  normal! gv""y
  let [text, @"] = [escape(@", '\/'), tmp]
  return '\V' .. substitute(text, "\n", '\\n', 'g')
endfunction

呪文のようだが、対象を検索して戻ってからcgnしているのは同じ。

選択したテキストそのままを置換するためにエスケープしたり\V(very nomagic)を使ったりしている。
改行が含まれていても動作するのが嬉しい。

ちなみに、cの代わりにvim-operator-replaceでもできる。

nnoremap <Plug>(builtin-/) /
nnoremap <Plug>(builtin-N) N
onoremap <Plug>(builtin-gn) gn
xmap <expr> <Space>r "\<ESC><Plug>(builtin-/)\<C-r>=<SID>search()\<CR>\<CR><Plug>(builtin-N)<Plug>(operator-replace)<Plug>(builtin-gn)"

かなり冗長に書いている。

<Plug>を使うのでxnoremapではなくxmapが必要になる。
もし/, N, gnにマッピングが存在したらそれが呼ばれてしまうので、元を呼ぶマッピングを作って回避している。
(<Plug>を使っての回避は行儀よくはないかも)
つまり回避が不要なら"\<ESC>/\<C-r>=<SID>search()\<CR>\<CR>N<Plug>(operator-replace)gn"になる。

:sを使うほどでもない、見たままのテキストを置き換えたいだけのときに思考停止できて便利。