1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Vimテキストオブジェクトの自作(手習い)

Last updated at Posted at 2020-11-12

概要

前回のオペレータの自作から引き続き、今度はテキストオブジェクトを自作してみようと思います。

目指すは、以下の2つです。

  • テキストオブジェクトの1文字目としての s を実現する。(surround.vim 的な)
  • テキストオブジェクトの2文字目としての % を実現する。(:h matchpairs参照)

とりあえずこの記事では、一つ目を実現します。

環境

GVIM 8.2.1287 (kaoriya 2020/07/24)

テキストオブジェクトの自作

巷には、テキストオブジェクトを追加するプラグインや、テキストオブジェクトの追加を容易にするためのプラグインなどが存在しています。

・・・が、あえてそれらは使いません。

それらのコードを読んだりもしていないので、自分が正しいことをやっている自信もありませんが、とりあえずそれっぽく実現できてしまったので、その過程を記事として書いていこうと思います。

※ドットリピートの実装方法だけ、repeat.vim をちょっとカンニング参考にさせてもらいました。

・・・職場は基本ネットに繋がらないので、プラグインマネージャを使用するなど夢のまた夢なのです。
スクリプトとして手打ちできる程度に、なるべくコンパクトに実装する必要があります。

人様の事を慮って、外部定義によりカスタマイズ可能にするような冗長性も必要ありません。
だって全部自作で使うのも自分だから。カスタマイズというよりも、もう直接修正します。

前回の「最後に」、にも書きましたが、オペレータを追加する方法としては、:h 'operatorfunc'というものが用意されていました。とても便利でした。

ですが、テキストオブジェクトの場合には、そういった優しい仕掛けは存在しないようなのです?

なので、オペレータ待機モードでマッピングを行って、自前で処理を書く必要があるのでは?と思い、:h map.txtを読むところから始めました。

まずは定番の、Vimヘルプの受け売り

まずは、:h omap-infoのセクションを読んでいたところ、このようなサンプルを発見しました。

onoremap <silent> F :<C-U>normal! 0f(hviw<CR>

オペレータ待機モードにマッピングを追加する。まさに私がやりたかったことそのものです。
相変わらずいいところを突いてくるニクいサンプルのチョイスで痺れます。

さっそく試してみました。

実際に試してみたい場合は、以下の手順を実行してください。

  • 以下の、omap_test.vimを適当なパスに保存して、Vimで直接開いてから、:so %する。
  • fu! s:hoge()の行のどこかで、cfとかdfとかvfとかしてみてください。
  • もしくは、fu! s:hoge()の行のどこかで、cFとかdFとかvFとかしてみてください。
omap_test.vim
" vi: set et ts=4 sts=4 sw=4 fdm=marker :
" ヘルプ受け売りそのまま(:h omap-info)

"関数名部分を選択。
onoremap <silent> f :<C-U>normal! 0f(hviw<CR>
"上記の動きを、visualモード時にも行うように。
vnoremap <silent> f :<C-U>normal! 0f(hviw<CR>

"上記を応用して、大文字バリエーション追加(括弧直前の1Wordではなく、空白から括弧までを対象)
onoremap <silent> F :<C-U>normal! 0f(hBvt(<CR>
vnoremap <silent> F :<C-U>normal! 0f(hBvt(<CR>

fu! s:hoge()
	echo '上の行でdfは"hoge"が削除。cやyも使えるし、vfなら"hoge"が選択される'
	echo '大文字のFなら、"s:hoge"が対象になる。'
endf

・・・実際にはこんなものを定義してしまうと、f{char}F{char}による行内の左右方向検索が効かなくなってしまって、ビックリするほど超不便になるだけなので、あくまでサンプルとして見るようにしてください。

確かに、関数定義の行のどこにカーソルが存在していても、関数名を対象にオペレータが効くようになります。
cfで関数名(hoge)が削除されてインサートモードに。
cFで関数名(s:hoge)が削除されてインサートモードに。
dfで関数名(hoge)が削除される。
dFで関数名(s:hoge)が削除される。
・・・みたいな。

vmapにも同じものを追加することで、v{motion}のような振る舞いも実現できました。
関数定義の行のどこにカーソルが存在しても、vfとやれば関数名部分がヴィジュアル選択されます。

というかマップの右辺値でやっていることはヴィジュアル選択なので、こちらの方がより直観的で素直なマッピングの発現なのでは?と思ったり。

因みにマッピング右辺値の<CR>が無いと、omapでもvmapでも、何も起こりません。

ヘルプのサンプルを試してみてわかったこと。

  • オペレータ待機モードで、ビジュアル選択して<CR>するようなマッピングを行うと、オペレータがその範囲を対象に処理を行ってくれる。
  • 不用意にマッピングを弄って、意図せず標準の機能を損なった場合、使用者の受ける衝撃と混乱は想像を絶する。

という事が分かった。

とくに2つ目は本当にビックリしましたね。

「?(あ、なんか変なタイポしたかも?)・・・!(またか!)・・・!?(いや何かおかしい!?)・・・??(え、なにこれ、え??)・・・!!(ああ!!)」

という感じでした。久々に強烈なアハ体験のような衝撃を脳に受けました。

1度目の挫折

ここまで試したところで、オペレータ待機モードのマッピングでは、一つ目の目的である「テキストオブジェクトの1文字目s」を定義するには、もう手遅れなのではないか?という疑問が生じました。

例えば、dsを定義したい場合、dで既にオペレータ待機モードに入っているのですが、そこにsをマッピングするとします。

関数を割り振るとしたら、こんな感じになります。

omap s :<C-U>call <SID>surround()<CR>

で、問題は、cの後のsだろうが、dの次のsだろうが、yの次のsだろうが、全部surround()が処理をしないといけなくなります。

csの次に入力されるのは、変更したい現在の囲み文字と、変更後の囲み文字の2つです。
dsの次に入力されるのは、削除したい現在の囲み文字です。
ysの次に入力されるのは、囲みたいテキストオブジェクトと、追加したい囲み文字です。

範囲選択しただけで、1文字目のcdyが発動するようなomapに乗せる関数では、上記のような仕掛けのために用いることは難しそうです。

そもそも、ysはyankの働きをしないので、yオペレータとして待機している時点で待ったをかける必要があります。

ちなみに、単打で打てるオペレータがcdyなので、囲い文字を変えたり消したりはともかく、囲い文字だけYankしたいってこたぁねぇだろ、ということで、ysが囲み追加オペレータになっているものと思われます。

sy等にすると、s出来なくなってしまうのでしょうか?私には恐ろしくてできません。。

とりあえず、surround.vim の真似をしてみる。

という事で、omapではなくmapに、csdsysを書いてしまおうという方針転換をしました。
※本来はnmapでやるべきなのでしょうが、この時点の筆者はまだ気が付いていませんでした。。

この時点から、オペレータ待機モードでのリマップ(omap)はもう出てきません。
ファイル名に偽りありです。

ひとまずガーッと書いてみました。

  • ddが遅延するのが気持ち悪い! (noremapのせいです)
  • ドットリピートができない! (g@は何も考えなくてもドットリピートできたのに・・・)
  • ysの動きが変! (この時はまだ気がついてもなかった)

という問題点を抱えたバージョンです。
生まれたての小鹿のようなものです。微笑ましいですね。

omap_test2.vim
" vi: set et ts=4 sts=4 sw=4 fdm=marker :

"互換性オプションを退避して規定値に設定
let s:save_cpo = &cpo
set cpo&vim

noremap <silent> cs :<C-U>call <SID>cSorround()<CR>
noremap <silent> ds :<C-U>call <SID>dSorround()<CR>
noremap <silent> ys :<C-U>call <SID>ySorround()<CR>

fu! s:cSorround() abort "{{{
    call s:sorround()
    let after = nr2char(getchar())
    call s:getAfter(after)

    call setpos('.',s:iEnd)
    normal! lv
    call setpos('.',s:aEnd)
    exec "normal! c".s:afterEnd

    call setpos('.',s:aStart)
    normal! v
    call setpos('.',s:iStart)
    normal! h
    exec "normal! c".s:afterStart

endf "}}}

fu! s:dSorround() abort "{{{
    call s:sorround()

    call setpos('.',s:iEnd)
    normal! lv
    call setpos('.',s:aEnd)
    normal! d

    call setpos('.',s:aStart)
    normal! v
    call setpos('.',s:iStart)
    normal! hd

endfu "}}}

fu! s:ySorround() abort "{{{
    call s:sorround()
    let after = nr2char(getchar())
    call s:getAfter(after)

    call setpos('.',s:iEnd)
    exec "normal! a".s:afterEnd

    call setpos('.',s:iStart)
    exec "normal! i".s:afterStart

endf "}}}

fu! s:getAfter(after) abort "{{{
    let flgMps = 0
    for mp in split(&mps,',')
        let pairs = split(mp,':')
        if a:after == pairs[0] || a:after == pairs[1]
            let flgMps = 1
            break
        endif
    endfor

    if flgMps == 0
        let s:afterStart = a:after
        let s:afterEnd   = a:after
    else
        let s:afterStart =pairs[0]
        let s:afterEnd   =pairs[1]
    endif
endf "}}}

fu! s:sorround() abort "{{{
    let what = nr2char(getchar())

    "すでにビジュアルモードの場合は抜ける。
    if mode() ==? "v"
        exec "normal! " . mode()
    endif

    exec "normal vi".what
    let s:iEnd = getcurpos()
    normal! o
    let s:iStart = getcurpos()
    normal! v

    exec "normal va".what
    let s:aEnd = getcurpos()
    normal! o
    let s:aStart = getcurpos()
    normal! v

    "範囲確認用
    "message clear
    "echom s:aStart[2]."-".s:iStart[2]."(".s:iStart[1].") , ".s:iEnd[2]."-".s:aEnd[2]."(".s:iEnd[1].")"
endf "}}}

"互換性オプションを復元
let &cpo = s:save_cpo

"test
"<a>'(hoge )'</a>
"<a>'(hoge )'</a>

キモは、s:surround()関数です。
getchar()により1文字入力を受け付けて、テキストオブジェクトの2文字目に挿げ替えて使用します。
例えば、csに続けてwが打たれた場合は、viwvawを行い、それぞれの選択開始・終了位置を保持します。

次にgetAfter()関数です。
こちらは、単一括り文字以外で、組になる括り文字の相方を探すための処理になります。
括弧などの組み合わせについては、:h 'matchpairs'を参照です。
vimrcに全角括弧系をvimrcに追加していたので、それも含めて処理してくれるようにしてあります。

ここまでできれば、もう完成したも同然でした。

まだこの時点では、ysの動きは本家と異なっています。
本家だとysiw'とするべきところが、ysw'となります。
ysaw'相当の振る舞いはまだ実現できていませんでした。

そんなことよりも、こんなに簡単にできちゃった!という喜びが、ドットリピートができない!と言うことに気づいた瞬間、絶望に塗り替えられました。

2度目の挫折

ドットリピートができない。ハッキリ言ってそんなコマンドにはあんまり意味がない。

もともとのsurround.vimはいったいどうだったんだ!・・・と調べてみました。

どうやら単体では対応しておらず、別途プラグインを導入することで実現しているようでした。
何ということでしょう。surround.vimは中身を読んでなかったから、そんなことにも気が付きいていませんでした。

当然対応しているものとばかり・・・。

早速、repeat.vimの真似をしてみる。

流石に独自実装を貫こうなどという気概はどこかに吹き飛びました。(元からないけど)

というわけで、repeat.vimの中身を読んでみました。
.をリマップして、関数を呼び出しています。・・・なるほどなるほどー。

無我夢中にドットリピートに対応させました。

ついでにnoremapからnnoremapに変更しました。これでdd等が遅延して気持ち悪い問題も解決しました。
ysの挙動が変なのは、この時点ではまだ気付いてもいないので相変わらずです。

omap_test3.vim
" vi: set et ts=4 sts=4 sw=4 fdm=marker :

scriptencoding utf-8

"二重ロード避け
if exists("g:loaded_omap_test3")
  finish
endif
let g:loaded_omap_test3 = 1

"互換性オプションを退避して規定値に設定
let s:save_cpo = &cpo
set cpo&vim

au TextChanged,TextChangedI,TextChangedP,TextYankPost * call <SID>textChanged()
fu! s:textChanged() "{{{
    if exists("b:dotReady")
        let b:dotReady = ( b:dotReady==0 ? 0 : eval(b:dotReady - 1) )
    endif
endf "}}}

"nnoremapではなく noremap にしてしまうと、ddとかが遅延発動して気持ち悪くなるので注意。
nnoremap <silent> cs :<C-U>call <SID>cSorround(v:false)<CR>
nnoremap <silent> ds :<C-U>call <SID>dSorround(v:false)<CR>
nnoremap <silent> ys :<C-U>call <SID>ySorround(v:false)<CR>

fu! s:cSorround(dotRep) abort "{{{
    let b:dotRepeatFunc="cSorround"
    call s:sorround(a:dotRep)

    if a:dotRep == v:true && exists("b:after")
        let after = b:after
    else
        let after = nr2char(getchar())
        let b:after = after
    endif

    call s:getAfter(after)

    call setpos('.',s:iEnd)
    normal! lv
    call setpos('.',s:aEnd)
    exec "normal! c".s:afterEnd

    call setpos('.',s:aStart)
    normal! v
    call setpos('.',s:iStart)
    normal! h
    exec "normal! c".s:afterStart

    let b:dotReady = 2
endf "}}}

fu! s:dSorround(dotRep) abort "{{{
    let b:dotRepeatFunc="dSorround"
    call s:sorround(a:dotRep)

    call setpos('.',s:iEnd)
    normal! lv
    call setpos('.',s:aEnd)
    normal! d

    call setpos('.',s:aStart)
    normal! v
    call setpos('.',s:iStart)
    normal! hd

    let b:dotReady = 2
endfu "}}}

fu! s:ySorround(dotRep) abort "{{{
    let b:dotRepeatFunc="ySorround"
    call s:sorround(a:dotRep)

    if a:dotRep == v:true && exists("b:after")
        let after = b:after
    else
        let after = nr2char(getchar())
        let b:after = after
    endif

    call s:getAfter(after)

    call setpos('.',s:iEnd)
    exec "normal! a".s:afterEnd

    call setpos('.',s:iStart)
    exec "normal! i".s:afterStart

    let b:dotReady = 2
endf "}}}

fu! s:getAfter(after) abort "{{{
    let flgMps = 0
    for mp in split(&mps,',')
        let pairs = split(mp,':')
        if a:after == pairs[0] || a:after == pairs[1]
            let flgMps = 1
            break
        endif
    endfor

    if flgMps == 0
        let s:afterStart = a:after
        let s:afterEnd   = a:after
    else
        let s:afterStart =pairs[0]
        let s:afterEnd   =pairs[1]
    endif
endf "}}}

fu! s:sorround(dotRep) abort "{{{
    if a:dotRep == v:true && exists("b:what")
        let what = b:what
    else
        let what = nr2char(getchar())
        let b:what = what
    endif

    "すでにビジュアルモードの場合は抜ける。
    if mode() ==? "v"
        exec "normal! " . mode()
    endif

    exec "normal vi".what
    let s:iEnd = getcurpos()
    normal! o
    let s:iStart = getcurpos()
    normal! v

    exec "normal va".what
    let s:aEnd = getcurpos()
    normal! o
    let s:aStart = getcurpos()
    normal! v

    "範囲確認用
    "message clear
    "echom s:aStart[2]."-".s:iStart[2]."(".s:iStart[1].") , ".s:iEnd[2]."-".s:aEnd[2]."(".s:iEnd[1].")"
endf "}}}

nmap . :<C-U>call <SID>dotRepeat()<CR>
fu! s:dotRepeat() "{{{
    if exists("b:dotReady") && b:dotReady != 0
        exec "call <SID>".b:dotRepeatFunc."(".v:true.")"
    else
        normal! .
    endif
endf "}}}

"互換性オプションを復元
let &cpo = s:save_cpo

"test
"<a>'(hoge )'</a>
"<a>'(hoge )'</a>

これでドットリピートに対応することができました。

ドットリピートについては、dotRepeat()関数がキモになります。

  • csdsysが押されたときに、フラグを立てておいて、テキストが変更されたときそのフラグを下げる。
  • .押下時にそのフラグが立っている時は、さっき実行したのと同じ関数を実行する。
    という感じで実装しました。

1を立てておくと、自分自身が編集した事で発生するイベントで0になってしまうので、2を設定してあります。
なんだかかっこ悪いです。

3度目の挫折

実はまだ問題が残っており、アンドゥを挟むと、次のドットリピートが効かなくなります。
cs'"してから、.で繰り返していって、間違えたと思ったらuして、再度.すると、またcs'"が続いていってほしいじゃないですか。

続かないんですよ、これが。ファッキンこの役立たずめ!

再び、repeat.vimを読んでみる。

もう一度ザッと目を通したところ、uUもリマップされていました。
これでアンドゥ後のドットリピートも問題なく動くようにしているのではないかと推測されます。

マップされている関数から実装を追っかけてみようとしましたが、なんだかややこしかったので、自分なりに簡単に実現する方法を取りました。結果的にはたぶん似たような対応になったのではないかと思います?

このバージョンでは以下の対応をしました。

  • アンドゥされた後にもドットリピートが続くようにしました。
  • ysが受け取るものを、サラウンドオブジェクトではなく、テキストオブジェクトに変えました。
  • showcmdオプションでオペレータの入力途中までのストロークを表示してくれるのですが、自前の処理に入ると何も出なくなってしまうので、echoで表示するように対応。

    何のことを言っているのかは:h 'showcmd'参照の事。表示箇所が変わってしまうのがちょっと不満です。
omap_test4.vim
" vi: set et ts=4 sts=4 sw=4 fdm=marker :
" sorround.vimの真似事。ysの動きも合わせたつもり。タイポ時はu頼み。
" 後は、追加する囲みに、タグ<hoge>~</hoge>を対応させたい。

scriptencoding utf-8

"二重ロード避け
if exists("g:loaded_omap_test4")
  finish
endif
let g:loaded_omap_test4 = 1

"互換性オプションを退避して規定値に設定
let s:save_cpo = &cpo
set cpo&vim

au TextChanged,TextChangedI,TextChangedP,TextYankPost * call <SID>textChanged()
fu! s:textChanged() "{{{
    if exists("b:dotReady")
        "テキストが変更されたときに、-1する。
        let b:dotReady = ( b:dotReady==0 ? 0 : eval(b:dotReady - 1) )
    endif
endf "}}}

"nnoremapではなく noremap にしてしまうと、ddとかが遅延発動して気持ち悪くなるので注意。
nnoremap <silent> cs :<C-U>call <SID>cSorround(v:false)<CR>
nnoremap <silent> ds :<C-U>call <SID>dSorround(v:false)<CR>
nnoremap <silent> ys :<C-U>call <SID>ySorround(v:false)<CR>

fu! s:cSorround(dotRep) abort "{{{
    "ドットリピート用に関数名を保持
    let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
    let s:cmd = "cs"

    "囲い部分の範囲を取得
    call s:getSorround(a:dotRep)

    "変更後の囲い文字を取得
    if a:dotRep == v:true
        "ドットリピート時は前回値を使用
        let after = b:after
    else
        redraw!
        echo s:cmd.b:what
        let after = nr2char(getchar())
        let b:after = after
    endif

    "囲い末尾の取得
    call s:getAfter(after)

    "囲い終わり箇所の置き換え
    call setpos('.',s:iEnd)
    normal! lv
    call setpos('.',s:aEnd)
    exec "normal! c".s:afterEnd

    "囲い始め箇所の置き換え
    call setpos('.',s:aStart)
    normal! v
    call setpos('.',s:iStart)
    normal! h
    exec "normal! c".s:afterStart

    "自身の終了時にイベントで-1されるため、2を設定
    let b:dotReady = 2
    redraw!
endf "}}}

fu! s:dSorround(dotRep) abort "{{{
    "ドットリピート用に関数名を保持
    let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
    let s:cmd = "ds"

    "囲い部分の範囲を取得
    call s:getSorround(a:dotRep)

    "囲い終わり箇所を削除
    call setpos('.',s:iEnd)
    normal! lv
    call setpos('.',s:aEnd)
    normal! d

    "囲い始め箇所を削除
    call setpos('.',s:aStart)
    normal! v
    call setpos('.',s:iStart)
    normal! hd

    "自身の終了時にイベントで-1されるため、2を設定
    let b:dotReady = 2
    redraw!
endfu "}}}

fu! s:ySorround(dotRep) abort "{{{
    "ドットリピート用に関数名を保持
    let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
    let s:cmd = "ys"


    "囲う部分のテキストオブジェクト部分を取得
    call s:getMotion(a:dotRep)

    "追加する囲い文字を取得
    if a:dotRep == v:true
        "ドットリピート時は前回値を使用
        let after = b:after
    else
        redraw!
        echo s:cmd.b:count.b:where.b:what
        let after = nr2char(getchar())
        let b:after = after
    endif

    "変更後の(追加する)囲い文字を取得
    call s:getAfter(after)

    "囲い終わり箇所に追加
    call setpos('.',s:iEnd)
    exec "normal! a".s:afterEnd

    "囲い始め箇所に追加
    call setpos('.',s:iStart)
    exec "normal! i".s:afterStart

    "自身の終了時にイベントで-1されるため、2を設定
    let b:dotReady = 2
    redraw!
endf "}}}

fu! s:getAfter(after) abort "{{{
    let mps_save = &mps

    "アングルブラケットをマッチペアに追加 
    "TODO:「yaw<a>」で「hoge⇒<a>hoge</a>」みたいなのに対応する時には外す。
    "sの直後としては<:>はありだが、変更後としては<はタグの開始で、>のみの場合に、<:>括りとして扱うように。
    set mps+=<:>

    "全角カッコ系を追加(実験的)
    set mps+=(:),「:」,{:},[:],【:】,『:』,<:>,≪:≫,《:》

    "マッチペアを検索
    let flgMps = 0
    for mp in split(&mps,',')
        let pairs = split(mp,':')
        if a:after == pairs[0] || a:after == pairs[1]
            let flgMps = 1
            break
        endif
    endfor

    "マッチペアを戻す
    let &mps = mps_save

    if flgMps == 0
        "マッチペアに該当しない場合は、同じ文字を設定する。
        let s:afterStart = a:after
        let s:afterEnd   = a:after
    else
        "マッチペアに該当した場合は、組合せを設定する
        let s:afterStart =pairs[0]
        let s:afterEnd   =pairs[1]
    endif
endf "}}}

fu! s:getSorround(dotRep) abort "{{{

    "囲う対象を取得
    if a:dotRep == v:true
        let what = b:what
    else
        redraw!
        echo s:cmd
        let what = nr2char(getchar())
        let b:what = what
    endif

    "すでにビジュアルモードの場合、モードを終了する。(現状はnmapのみなのであり得ない想定)
    if mode() ==? "v"
        exec "normal! " . mode()
    endif

    "内側の範囲を取得
    exec "normal vi".what
    let s:iEnd = getcurpos()
    normal! o
    let s:iStart = getcurpos()
    normal! v

    "外側の範囲を取得
    exec "normal va".what
    let s:aEnd = getcurpos()
    normal! o
    let s:aStart = getcurpos()
    normal! v

    "範囲確認用
    "message clear
    "echom s:aStart[2]."-".s:iStart[2]."(".s:iStart[1].") , ".s:iEnd[2]."-".s:aEnd[2]."(".s:iEnd[1].")"
endf "}}}

fu! s:getMotion(dotRep) abort "{{{
    let l:count = ""
    let l:where = ""
    let l:what  = ""

    if a:dotRep == v:true
        "ドットリピート時は前回値を使用
        let l:count = b:count
        let l:where = b:where
        let l:what  = b:what
    else
        redraw! 
        echo s:cmd
        let wk = nr2char(getchar())
        while wk =~ "[0-9]"
            let l:count .= wk
            redraw!
            echo s:cmd.l:count
            let wk = nr2char(getchar())
        endwhile

        if wk =~ "[ia]"
            let l:where = wk
            redraw!
            echo s:cmd.l:count.l:where
            let l:what  = nr2char(getchar())
        else
            let l:what = wk
        endif
        redraw!
        echo s:cmd.l:count.l:where.l:what

        let b:count = l:count
        let b:where = l:where
        let b:what  = l:what
    endif

    "内側の範囲を取得
    exec "normal v".l:count.l:where.l:what
    let s:iEnd = getcurpos()
    normal! o
    let s:iStart = getcurpos()
    normal! v

endf '}}}

nmap . :<C-U>call <SID>dotRepeat()<CR>
fu! s:dotRepeat() "{{{
    if exists("b:dotReady") && b:dotReady != 0
        silent exec "call <SID>".b:dotRepeatFunc."(".v:true.")"
        redraw!
    else
        normal! .
    endif
endf "}}}

nmap u :<C-U>call <SID>undoRepeat("u")<CR>
nmap U :<C-U>call <SID>undoRepeat("U")<CR>
fu! s:undoRepeat(cmdUndo) "{{{
    if exists("b:dotReady") && b:dotReady != 0
        let b:dotReady = 2
    endif
    exec "normal! ".a:cmdUndo
endf "}}}

fu! s:funcName(sfile) "{{{
    return substitute(a:sfile,".*_","","")
endf "}}}

"互換性オプションを復元
let &cpo = s:save_cpo

"test
"<a>'(hoge hoge )'</a>
"<a>'(hoge hoge )'</a>
"<a>'(hoge hoge )'</a>

ドットリピートをアンドゥ後にも有効にするためには、undoRepeat()関数がキモになります。
uまたはUが押されたときに、ドットリピート用のフラグを再設定してから、アンドゥするようにしてあります。

また、ドットリピート用の関数名をベタ書きにしていましたが、expand("<sfile>")を用いて取るように修正しました。

コメント文も徐々に充実してきましたね。

4度目の挫折

ysが多少まともに動くようになったことにより、「タグで囲めない。」という事にようやく気が付きました。

例えば、ysiwtとした時(することは無いと思いますが)などは、tという文字そのもので囲まれてしまいます。

hoge

これを、ysiwt とすると、

thoget

こうなります。まるで意味がありません。(そもそも追加するものとしてtというのが使い方が間違っているので当然なのですが)
正しく追加したいタグを指定して、ysiw<p>とやろうと思っても、ysiw<まで入力した時点で、

hoge

が、

<hoge>

となります。

同様に、cst<と入力した時点で、既存のタグが消えて、<>で囲まれてしまいます。

<p>hoge</p>

これに対して、cst<と 打つと、

<hoge>

こうなる。

置換元のstによるタグの範囲指定はうまくいっているのですが、置換後の文字列としてタグを入力することができないのです。
これは変更後/追加したい囲い文字の入力を受け付ける時に、括弧などの組になる物の対を取得している関数を用意してあるので、簡単に対応できそうです。

テキストオブジェクトの入力を自前で受け付けてみる

変更後や、追加する囲い文字を入力するときに、<で始まった場合は、>まで溜めて、編集するように修正しました。
<>で囲いたい場合は、>を入れればOK。

ついでに、行数の削減を少々行いました。

omap_test5.vim
" vi: set et ts=4 sts=4 sw=4 fdm=marker :
" sorround.vimの真似事。ysの動きも合わせたつもり。タイポ時はu頼み。

scriptencoding utf-8

"二重ロード避け
if exists("g:loaded_omap_test5")
  finish
endif
let g:loaded_omap_test5 = 1

"互換性オプションを退避して規定値に設定
let s:save_cpo = &cpo
set cpo&vim

au TextChanged,TextChangedI,TextChangedP,TextYankPost * call <SID>textChanged()
fu! s:textChanged() "{{{
    if exists("b:dotReady")
        "テキストが変更されたときに、-1する。
        let b:dotReady = ( b:dotReady==0 ? 0 : eval(b:dotReady - 1) )
    endif
endf "}}}

"nnoremapではなく noremap にしてしまうと、ddとかが遅延発動して気持ち悪くなるので注意。
nnoremap <silent> cs :<C-U>call <SID>cSorround(v:false)<CR>
nnoremap <silent> ds :<C-U>call <SID>dSorround(v:false)<CR>
nnoremap <silent> ys :<C-U>call <SID>ySorround(v:false)<CR>

fu! s:cSorround(dotRep) abort "{{{
    "ドットリピート用に関数名を保持
    let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
    let s:cmd = "cs"

    "囲い部分の範囲を取得
    call s:getSorround(a:dotRep)

    "ドットリピート時は再取得しない
    if a:dotRep == v:false
        "変更後の囲い文字を取得
        call s:getAfter()
    endif

    "囲い終わり箇所の置き換え
    call setpos('.',s:iEnd) | normal! lv
    call setpos('.',s:aEnd)
    exec "normal! c".b:afterEnd

    "囲い始め箇所の置き換え
    call setpos('.',s:aStart) | normal! v
    call setpos('.',s:iStart) | normal! h
    exec "normal! c".b:afterStart

    "自身の終了時にイベントで-1されるため、2を設定
    let b:dotReady = 2 | redraw!
endf "}}}

fu! s:dSorround(dotRep) abort "{{{
    "ドットリピート用に関数名を保持
    let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
    let s:cmd = "ds"

    "囲い部分の範囲を取得
    call s:getSorround(a:dotRep)

    "囲い終わり箇所を削除
    call setpos('.',s:iEnd) | normal! lv
    call setpos('.',s:aEnd) | normal! d

    "囲い始め箇所を削除
    call setpos('.',s:aStart) | normal! v
    call setpos('.',s:iStart) | normal! hd

    "自身の終了時にイベントで-1されるため、2を設定
    let b:dotReady = 2 | redraw!
endfu "}}}

fu! s:ySorround(dotRep) abort "{{{
    "ドットリピート用に関数名を保持
    let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
    let s:cmd = "ys"

    "囲う部分のテキストオブジェクト部分を取得
    call s:getMotion(a:dotRep)

    "ドットリピート時は再取得しない
    if a:dotRep == v:false
        "追加する囲い文字を取得
        call s:getAfter()
    endif

    "囲いを追加
    call setpos('.',s:iEnd)     | exec "normal! a".b:afterEnd
    call setpos('.',s:iStart)   | exec "normal! i".b:afterStart

    "自身の終了時にイベントで-1されるため、2を設定
    let b:dotReady = 2 | redraw!
endf "}}}

fu! s:getAfter() abort "{{{
    redraw! | echo s:cmd
    let wk = nr2char(getchar())

    if wk == '<'
        "タグの場合は、開始タグ・終了タグ
        let l:tagStart =""
        while wk != '>'
            let l:tagStart .= wk
            redraw! | echo s:cmd.l:tagStart
            let wk = nr2char(getchar())
        endwhile
        let l:tagStart .= wk
        redraw! | echo s:cmd.l:tagStart

        let b:afterStart = l:tagStart
        let b:afterEnd   = substitute(l:tagStart,'<','</','')

    else
        "タグ以外の場合は、対になる記号、または、同一記号
        let after = wk
        let mps_save = &mps
        "アングルブラケットをマッチペアに追加※「<」はタグ入力のトリガーなので、「>」
        set mps+=<:>

        "全角カッコ系を追加(実験的)
        set mps+=(:),「:」,{:},[:],【:】,『:』,<:>,≪:≫,《:》

        "マッチペアを検索
        let flgMps = 0
        for mp in split(&mps,',')
            let pairs = split(mp,':')
            if after == pairs[0] || after == pairs[1]
                let flgMps = 1 | break
            endif
        endfor

        "マッチペアを戻す
        let &mps = mps_save

        if flgMps == 0
            "マッチペアに該当しない場合は、同じ文字を設定する。
            let b:afterStart = after
            let b:afterEnd   = after
        else
            "マッチペアに該当した場合は、組合せを設定する
            let b:afterStart =pairs[0]
            let b:afterEnd   =pairs[1]
        endif
    endif

endf "}}}

fu! s:getSorround(dotRep) abort "{{{

    "囲う対象を取得
    if a:dotRep == v:true
        "ドットリピート時は前回値を使用
        let what = b:what
    else
        redraw! | echo s:cmd
        let what = nr2char(getchar())
        let b:what = what
        let s:cmd .= what
    endif

    "すでにビジュアルモードの場合、モードを終了する。(現状はnmapのみなのであり得ない想定)
    if mode() ==? "v"
        exec "normal! " . mode()
    endif

    "内側の範囲を取得
    exec "normal vi".what
    let s:iEnd = getcurpos()    | normal! o
    let s:iStart = getcurpos()  | normal! v

    "外側の範囲を取得
    exec "normal va".what
    let s:aEnd = getcurpos()    | normal! o
    let s:aStart = getcurpos()  | normal! v

    "範囲確認用
    "message clear
    "echom s:aStart[2]."-".s:iStart[2]."(".s:iStart[1].") , ".s:iEnd[2]."-".s:aEnd[2]."(".s:iEnd[1].")"
endf "}}}

fu! s:getMotion(dotRep) abort "{{{
    let l:count = ""
    let l:where = ""
    let l:what  = ""

    "モーション文字列を取得
    if a:dotRep == v:true
        "ドットリピート時は前回値を使用
        let l:count = b:count
        let l:where = b:where
        let l:what  = b:what
    else
        redraw! | echo s:cmd
        let wk = nr2char(getchar())
        while wk =~ "[0-9]"
            let l:count .= wk
            redraw! | echo s:cmd.l:count
            let wk = nr2char(getchar())
        endwhile

        if wk =~ "[ia]"
            let l:where = wk
            redraw! | echo s:cmd.l:count.l:where
            let l:what  = nr2char(getchar())
        else
            let l:what = wk
        endif
        redraw! | echo s:cmd.l:count.l:where.l:what

        let b:count = l:count
        let b:where = l:where
        let b:what  = l:what

        let s:cmd  .= l:count.l:where.l:what
    endif

    "モーションの内側の範囲を取得
    exec "normal v".l:count.l:where.l:what
    let s:iEnd = getcurpos()    | normal! o
    let s:iStart = getcurpos()  | normal! v

endf '}}}

nmap . :<C-U>call <SID>dotRepeat()<CR>
fu! s:dotRepeat() "{{{
    if exists("b:dotReady") && b:dotReady != 0
        silent exec "call <SID>".b:dotRepeatFunc."(".v:true.")" | redraw!
    else
        normal! .
    endif
endf "}}}

nmap u :<C-U>call <SID>undoRepeat("u")<CR>
nmap U :<C-U>call <SID>undoRepeat("U")<CR>
fu! s:undoRepeat(cmdUndo) "{{{
    if exists("b:dotReady") && b:dotReady != 0
        let b:dotReady = 2
    endif
    exec "normal! ".a:cmdUndo
endf "}}}

fu! s:funcName(sfile) "{{{
    return substitute(a:sfile,".*_","","")
endf "}}}

"互換性オプションを復元
let &cpo = s:save_cpo

"test
"<a>'(hoge hoge )'</a>
"<a>'(hoge hoge )'</a>
"<a>'(hoge hoge )'</a>

ようやく満足できました。

後は本家にあって、出来ていないことはヴィジュアルモードでSというものですが、リピートで繰り返しづらそうですし、あまり必要性を感じないので、surround関係については、ここまででいったん満足しました。

%の実験に移る前に、しばらく気の向くままに機能追加を行っていきます。

ソースの体裁を整える

ソースを見直しつつ、誤記修正や、オペレータとしての充実を図りました。

  • スクリプト内のsorrundという恥ずかしい誤記をsurroundに修正しました。
  • マーカー折り畳みにより、スクリプトソースをブロック分けしました。
  • オペレータgusgUsg~sを追加しました。

    囲い文字の小文字と大文字を置き換えます。タグにしか意味がありません。

    タグを大文字にすることもあまりないと思いますが、とりあえずセットで作っておきました。
  • オペレータgzsgZsを追加しました。

    hz_ja.vimが必要です。エラーチェックとかしてないので、無い場合はどうなるのか不明

    囲い文字の全角と半角を変換できます!使い道は後から着いてくるはず!

    ~~ただし、小文字→大文字は簡単ですが、大文字→小文字に戻したい場合は、わざわざ全角で囲い文字を入力する必要があります。使い勝手が悪すぎる!~~対応してありました。自分のしたことを忘れている。。
omap_test6.vim
" vi: set et ts=4 sts=4 sw=4 fdm=marker :

scriptencoding utf-8

"二重ロード避け
if exists("g:loaded_omap_test6")
  finish
endif
let g:loaded_omap_test6 = 1

"互換性オプションを退避して規定値に設定
let s:save_cpo = &cpo
set cpo&vim

"マッピング{{{
    "変更、削除、追加{{{
    nnoremap <silent> cs :<C-U>call <SID>cSurround(v:false)<CR>
    nnoremap <silent> ds :<C-U>call <SID>dSurround(v:false)<CR>
    nnoremap <silent> ys :<C-U>call <SID>ySurround(v:false)<CR>
    "}}}

    "大文字小文字を変える。※タグ以外に意味はない(大文字にすることはあまり無いだろうけどセットで){{{
    nnoremap <silent> gus :<C-U>call <SID>guSurround(v:false,'u')<CR>
    nnoremap <silent> gUs :<C-U>call <SID>guSurround(v:false,'U')<CR>
    nnoremap <silent> g~s :<C-U>call <SID>guSurround(v:false,'~')<CR>
    "}}}

    "全角半角を変える(z半角,Z全角) ※タグを全角にすることはできるが(しないと思うが)、半角に戻すことはできない。{{{
    nnoremap <silent> gzs :<C-U>call <SID>gzSurround(v:false,'z')<CR>
    nnoremap <silent> gZs :<C-U>call <SID>gzSurround(v:false,'Z')<CR>
    "hz_ja.vim が必要(gHLとgZLを使用){{{
        "gHL可能な文字を全て半角に変換する
        "gZL 可能な文字を全て全角に変換する
        "gHA ASCII文字を全て半角に変換する
        "gHH ASCII文字を全て半角に変換する
        "gZA ASCII文字を全て全角に変換する
        "gHM 記号を全て半角に変換する
        "gZM 記号を全て全角に変換する
        "gHW 英数字を全て半角に変換する
        "gZW 英数字を全て全角に変換する
        "gHJ カタカナを全て半角に変換する
        "gZJ カタカナを全て全角に変換する
        "gZZ カタカナを全て全角に変換する
    "}}}
    "}}}

    "リピート対応{{{
    nmap . :<C-U>call <SID>dotRepeat()<CR>
    nmap u :<C-U>call <SID>undoRepeat("u")<CR>
    nmap U :<C-U>call <SID>undoRepeat("U")<CR>
    "}}}

"}}}

"イベント{{{
    au TextChanged,TextChangedI,TextChangedP,TextYankPost * call <SID>textChanged()
    fu! s:textChanged() "{{{
        if exists("b:dotReady")
            "テキストが変更されたときに、-1する。
            let b:dotReady = ( b:dotReady==0 ? 0 : eval(b:dotReady - 1) )
        endif
    endf "}}}
"}}}

"オペレータ主処理{{{
    fu! s:cSurround(dotRep) abort "{{{
        "ドットリピート用に関数名を保持
        let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
        let s:cmd = "cs"

        "囲い部分の範囲を取得
        call s:getSurround(a:dotRep)

        "ドットリピート時は再取得しない
        if a:dotRep == v:false
            "変更後の囲い文字を取得
            call s:getAfter()
        endif

        "囲い終わり箇所の置き換え
        call setpos('.',s:iEnd) | normal! lv
        call setpos('.',s:aEnd)
        exec "normal! c".b:afterEnd

        "囲い始め箇所の置き換え
        call setpos('.',s:aStart) | normal! v
        call setpos('.',s:iStart) | normal! h
        exec "normal! c".b:afterStart

        "自身の終了時にイベントで-1されるため、2を設定
        let b:dotReady = 2 | redraw!
    endf "}}}

    fu! s:dSurround(dotRep) abort "{{{
        "ドットリピート用に関数名を保持
        let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
        let s:cmd = "ds"

        "囲い部分の範囲を取得
        call s:getSurround(a:dotRep)

        "囲い終わり箇所を削除
        call setpos('.',s:iEnd) | normal! lv
        call setpos('.',s:aEnd) | normal! d

        "囲い始め箇所を削除
        call setpos('.',s:aStart) | normal! v
        call setpos('.',s:iStart) | normal! hd

        "自身の終了時にイベントで-1されるため、2を設定
        let b:dotReady = 2 | redraw!
    endfu "}}}

    fu! s:ySurround(dotRep) abort "{{{
        "ドットリピート用に関数名を保持
        let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
        let s:cmd = "ys"

        "囲う部分のテキストオブジェクト部分を取得
        call s:getMotion(a:dotRep)

        "ドットリピート時は再取得しない
        if a:dotRep == v:false
            "追加する囲い文字を取得
            call s:getAfter()
        endif

        "囲いを追加
        call setpos('.',s:iEnd)     | exec "normal! a".b:afterEnd
        call setpos('.',s:iStart)   | exec "normal! i".b:afterStart

        "自身の終了時にイベントで-1されるため、2を設定
        let b:dotReady = 2 | redraw!
    endf "}}}

    fu! s:guSurround(dotRep,...) abort "{{{
        "ドットリピート用に関数名を保持
        let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
        if a:0
            let b:case = a:1
        endif
        let s:cmd = "g".b:case."s"


        "囲い部分の範囲を取得
        call s:getSurround(a:dotRep)

        "囲い終わり箇所を処理
        call setpos('.',s:iEnd) | normal! lv
        exec "call setpos('.',s:aEnd) | normal! g".b:case

        "囲い始め箇所を処理
        call setpos('.',s:aStart) | normal! v
        exec "call setpos('.',s:iStart) | normal! hg".b:case

        "自身の終了時にイベントで-1されるため、2を設定
        let b:dotReady = 2 | redraw!
    endfu "}}}

    fu! s:gzSurround(dotRep,...) abort "{{{
        "ドットリピート用に関数名を保持
        let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
        if a:0
            let b:case = a:1
        endif
        let s:cmd = "g".b:case."s"

        if b:case ==# 'z'
            let l:hz = 'HL'
        else
            let l:hz = 'ZL'
        endif

        "囲い部分の範囲を取得
        call s:getSurround(a:dotRep)

        "囲い終わり箇所を処理
        call setpos('.',s:iEnd) | normal! lv
        exec "call setpos('.',s:aEnd) | normal g".l:hz

        "囲い始め箇所を処理
        call setpos('.',s:aStart) | normal! v
        exec "call setpos('.',s:iStart) | normal hg".l:hz

        "自身の終了時にイベントで-1されるため、2を設定
        let b:dotReady = 2 | redraw!
    endfu "}}}
"}}}

"共通処理{{{
    "現在の囲い文字の入力を受け付けて、その範囲を取得する
    fu! s:getSurround(dotRep) abort "{{{

        "囲う対象を取得
        if a:dotRep == v:true
            "ドットリピート時は前回値を使用
            let what = b:what
        else
            redraw! | echo s:cmd
            let what = nr2char(getchar())
            let b:what = what
            let s:cmd .= what
        endif

        "すでにビジュアルモードの場合、モードを終了する。(現状はnmapのみなのであり得ない想定)
        if mode() ==? "v"
            exec "normal! " . mode()
        endif

        if what =~ "[[:graph:]]"
            "ASCIIの場合
            "※nomalに!を付けないことで、他で定義されたtext-objectを有効にする

            "内側の範囲を取得
            exec "normal vi".what
            let s:iEnd = getcurpos()    | normal! o
            let s:iStart = getcurpos()  | normal! v


            "外側の範囲を取得
            exec "normal va".what
            let s:aEnd = getcurpos()    | normal! o
            let s:aStart = getcurpos()  | normal! v
        endif

        "半角文字としてテキストオブジェクトが見つからなかった場合、全角文字扱いにして再検索させる。
        if s:iStart == s:iEnd
            let what = ToZenkaku(what)
        endif

        "非ASCII(全角)の場合
        if what !~ "[[:graph:]]"

            "開始位置を取得
            let pairs = s:getPairs(what)
            exec "normal! F". pairs[0]
            let s:aStart = getcurpos() | normal! l
            let s:iStart = getcurpos()

            "終了位置を取得
            exec "normal! f". pairs[1]
            let s:aEnd = getcurpos() | normal! h
            let s:iEnd = getcurpos()
        endif


        "範囲確認用
        "message clear
        "echom s:aStart[2]."-".s:iStart[2]."(".s:iStart[1].") , ".s:iEnd[2]."-".s:aEnd[2]."(".s:iEnd[1].")"
    endf "}}}

    "変更後/追加する囲い文字の入力を受け付ける
    fu! s:getAfter() abort "{{{
        redraw! | echo s:cmd
        let wk = nr2char(getchar())

        if wk == '<'
            "タグの場合は、開始タグ・終了タグ
            let l:tagStart =""
            while wk != '>'
                let l:tagStart .= wk
                redraw! | echo s:cmd.l:tagStart
                let wk = nr2char(getchar())
            endwhile
            let l:tagStart .= wk
            redraw! | echo s:cmd.l:tagStart

            let b:afterStart = l:tagStart
            let b:afterEnd   = substitute(l:tagStart,'<','</','')

        else
            "タグ以外の場合は、対になる記号、または、同一記号
            let pairs = s:getPairs(wk)
            let b:afterStart =pairs[0]
            let b:afterEnd   =pairs[1]
        endif

    endf "}}}

    "囲い文字の組み合わせを取得する
    fu! s:getPairs(surround) "{{{
        let mps_save = &mps
        "アングルブラケットをマッチペアに追加※「<」はタグ入力のトリガーなので、「>」用
        set mps+=<:>

        "全角カッコ系を追加
        set mps+=(:),「:」,{:},[:],【:】,『:』,<:>,≪:≫,《:》

        "マッチペアを検索
        let flgMps = 0
        for mp in split(&mps,',')
            let pairs = split(mp,':')
            if a:surround == pairs[0] || a:surround == pairs[1]
                let flgMps = 1 | break
            endif
        endfor

        "マッチペアを戻す
        let &mps = mps_save

        if flgMps == 0
            "マッチペアに該当しない場合は、同じ文字を設定する。
            let pairs[0] = a:surround
            let pairs[1] = a:surround
        endif

        return pairs

    endf "}}}

    "変更前のテキストオブジェクトの入力を受け付ける
    fu! s:getMotion(dotRep) abort "{{{
        let l:count = ""
        let l:where = ""
        let l:what  = ""

        "モーション文字列を取得
        if a:dotRep == v:true
            "ドットリピート時は前回値を使用
            let l:count = b:count
            let l:where = b:where
            let l:what  = b:what
        else
            redraw! | echo s:cmd
            let wk = nr2char(getchar())
            while wk =~ "[0-9]"
                let l:count .= wk
                redraw! | echo s:cmd.l:count
                let wk = nr2char(getchar())
            endwhile

            if wk =~ "[ia]"
                let l:where = wk
                redraw! | echo s:cmd.l:count.l:where
                let l:what  = nr2char(getchar())
            else
                let l:what = wk
            endif
            redraw! | echo s:cmd.l:count.l:where.l:what

            let b:count = l:count
            let b:where = l:where
            let b:what  = l:what

            let s:cmd  .= l:count.l:where.l:what
        endif

        "モーションの内側の範囲を取得
        exec "normal v".l:count.l:where.l:what
        let s:iEnd = getcurpos()    | normal! o
        let s:iStart = getcurpos()  | normal! v

    endf '}}}
"}}}

"ドットリピート関連{{{
    "リピート
    fu! s:dotRepeat() "{{{
        if exists("b:dotReady") && b:dotReady != 0
            silent exec "call <SID>".b:dotRepeatFunc."(".v:true.")" | redraw!
        else
            normal! .
        endif
    endf "}}}

    "アンドゥ後のリピートに備える
    fu! s:undoRepeat(cmdUndo) "{{{
        if exists("b:dotReady") && b:dotReady != 0
            let b:dotReady = 2
        endif
        exec "normal! ".a:cmdUndo
    endf "}}}

    "リピート時の関数名を取得する
    fu! s:funcName(sfile) "{{{
        return substitute(a:sfile,".*_","","")
    endf "}}}
"}}}

"互換性オプションを復元
let &cpo = s:save_cpo

"test
"<a>'(hoge hoge )'</a>
"<a>'(hoge hoge )'</a>
"<a>'(hoge hoge )'</a>

改善点を見つけてみる。

全角半角変換は、kaoriya版には最初からついているものなのですが、ヴィジュアルモードから発動するシフト同時押しを多用する大文字が多めな若干押しづらいコマンド群と、コマンドラインモードから使用する、やや冗長な長さのコマンドからしか呼び出すことができませんでした。

たまに使って便利だなと思うことがあっても、しばらく使わないとすぐに忘れてしまって、皆さんどうやって使っているのだろうと常々疑問でした。たぶん私以外の人は記憶力が素晴らしくて、たまにしか使わないものでも忘れないのであろうなぁ、などと悲嘆に暮れていたものです。

こうしてスクリプトから呼び出す形で実装してみたところで、普段から使っている人は何らかのマッピングを行って使っているのではなかろうか?と言うことに、ようやく思い至りました。

今回こうして、全角半角を変換する機能が、サラウンドオブジェクトを対象に通常モードのコマンドで実行できるようになりました。ですが、普通のテキストオブジェクトに対しては実行出来ないのです。チグハグで頂けません。早速対応してみましょう。

改善してみた。

gzSurround()関数をgzOperator()関数に名前を変えました。
そして、「gzsgZs」だけではなく、「gzi gZi gza gZa」を追加しました。

omap_test7.vim
" vi: set et ts=4 sts=4 sw=4 fdm=marker :

scriptencoding utf-8

"二重ロード避け
if exists("g:loaded_omap_test7")
  finish
endif
let g:loaded_omap_test7 = 1

"互換性オプションを退避して規定値に設定
let s:save_cpo = &cpo
set cpo&vim

"マッピング{{{
    "変更、削除、追加{{{
    nnoremap <silent> cs :<C-U>call <SID>cSurround(v:false)<CR>
    nnoremap <silent> ds :<C-U>call <SID>dSurround(v:false)<CR>
    nnoremap <silent> ys :<C-U>call <SID>ySurround(v:false)<CR>
    "}}}

    "大文字小文字を変える。※タグ以外に意味はない(大文字にすることはあまり無いだろうけどセットで){{{
    nnoremap <silent> gus :<C-U>call <SID>guSurround(v:false,'u')<CR>
    nnoremap <silent> gUs :<C-U>call <SID>guSurround(v:false,'U')<CR>
    nnoremap <silent> g~s :<C-U>call <SID>guSurround(v:false,'~')<CR>
    "}}}

    "全角半角を変える(z半角,Z全角) ※タグを全角にすることはできるが(しないと思うが)、半角に戻すことはできない。{{{
    nnoremap <silent> gzs :<C-U>call <SID>gzOperator(v:false,'z','s')<CR>
    nnoremap <silent> gZs :<C-U>call <SID>gzOperator(v:false,'Z','s')<CR>
    "ついでに
    nnoremap <silent> gzi :<C-U>call <SID>gzOperator(v:false,'z','i')<CR>
    nnoremap <silent> gZi :<C-U>call <SID>gzOperator(v:false,'Z','i')<CR>
    nnoremap <silent> gza :<C-U>call <SID>gzOperator(v:false,'z','a')<CR>
    nnoremap <silent> gZa :<C-U>call <SID>gzOperator(v:false,'Z','a')<CR>

    "hz_ja.vim が必要(gHLとgZLを使用){{{
        "gHL可能な文字を全て半角に変換する
        "gZL 可能な文字を全て全角に変換する
        "gHA ASCII文字を全て半角に変換する
        "gHH ASCII文字を全て半角に変換する
        "gZA ASCII文字を全て全角に変換する
        "gHM 記号を全て半角に変換する
        "gZM 記号を全て全角に変換する
        "gHW 英数字を全て半角に変換する
        "gZW 英数字を全て全角に変換する
        "gHJ カタカナを全て半角に変換する
        "gZJ カタカナを全て全角に変換する
        "gZZ カタカナを全て全角に変換する
    "}}}
    "}}}

    "リピート対応{{{
    nmap . :<C-U>call <SID>dotRepeat()<CR>
    nmap u :<C-U>call <SID>undoRepeat("u")<CR>
    nmap U :<C-U>call <SID>undoRepeat("U")<CR>
    "}}}

"}}}

"イベント{{{
    au TextChanged,TextChangedI,TextChangedP,TextYankPost * call <SID>textChanged()
    fu! s:textChanged() "{{{
        if exists("b:dotReady")
            "テキストが変更されたときに、-1する。
            let b:dotReady = ( b:dotReady==0 ? 0 : eval(b:dotReady - 1) )
        endif
    endf "}}}
"}}}

"オペレータ主処理{{{
    fu! s:cSurround(dotRep) abort "{{{
        "ドットリピート用に関数名を保持
        let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
        let s:cmd = "cs"

        "囲い部分の範囲を取得
        call s:getSurround(a:dotRep)

        "ドットリピート時は再取得しない
        if a:dotRep == v:false
            "変更後の囲い文字を取得
            call s:getAfter()
        endif

        "囲い終わり箇所の置き換え
        call setpos('.',s:iEnd) | normal! lv
        call setpos('.',s:aEnd)
        exec "normal! c".b:afterEnd

        "囲い始め箇所の置き換え
        call setpos('.',s:aStart) | normal! v
        call setpos('.',s:iStart) | normal! h
        exec "normal! c".b:afterStart

        "自身の終了時にイベントで-1されるため、2を設定
        let b:dotReady = 2 | redraw!
    endf "}}}

    fu! s:dSurround(dotRep) abort "{{{
        "ドットリピート用に関数名を保持
        let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
        let s:cmd = "ds"

        "囲い部分の範囲を取得
        call s:getSurround(a:dotRep)

        "囲い終わり箇所を削除
        call setpos('.',s:iEnd) | normal! lv
        call setpos('.',s:aEnd) | normal! d

        "囲い始め箇所を削除
        call setpos('.',s:aStart) | normal! v
        call setpos('.',s:iStart) | normal! hd

        "自身の終了時にイベントで-1されるため、2を設定
        let b:dotReady = 2 | redraw!
    endfu "}}}

    fu! s:ySurround(dotRep) abort "{{{
        "ドットリピート用に関数名を保持
        let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
        let s:cmd = "ys"

        "囲う部分のテキストオブジェクト部分を取得
        call s:getMotion(a:dotRep)

        "ドットリピート時は再取得しない
        if a:dotRep == v:false
            "追加する囲い文字を取得
            call s:getAfter()
        endif

        "囲いを追加
        call setpos('.',s:iEnd)     | exec "normal! a".b:afterEnd
        call setpos('.',s:iStart)   | exec "normal! i".b:afterStart

        "自身の終了時にイベントで-1されるため、2を設定
        let b:dotReady = 2 | redraw!
    endf "}}}

    fu! s:guSurround(dotRep,...) abort "{{{
        "ドットリピート用に関数名を保持
        let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
        if a:0
            let b:case = a:1
        endif
        let s:cmd = "g".b:case."s"


        "囲い部分の範囲を取得
        call s:getSurround(a:dotRep)

        "囲い終わり箇所を処理
        call setpos('.',s:iEnd) | normal! lv
        exec "call setpos('.',s:aEnd) | normal! g".b:case

        "囲い始め箇所を処理
        call setpos('.',s:aStart) | normal! v
        exec "call setpos('.',s:iStart) | normal! hg".b:case

        "自身の終了時にイベントで-1されるため、2を設定
        let b:dotReady = 2 | redraw!
    endfu "}}}

    fu! s:gzOperator(dotRep,...) abort "{{{
        "ドットリピート用に関数名を保持
        let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
        if a:0
            let b:case = a:1
            let b:gzWhere = a:2
        endif
        let s:cmd = "g".b:case.b:gzWhere

        if b:case ==# 'z'
            let l:hz = 'HL'
        else
            let l:hz = 'ZL'
        endif

        if b:gzWhere == 's'
            "囲い部分の範囲を取得
            call s:getSurround(a:dotRep)

            "囲い終わり箇所を処理
            call setpos('.',s:iEnd) | normal! lv
            exec "call setpos('.',s:aEnd) | normal g".l:hz

            "囲い始め箇所を処理
            call setpos('.',s:aStart) | normal! v
            exec "call setpos('.',s:iStart) | normal hg".l:hz
        else
            if a:dotRep != v:true
                redraw! | echo s:cmd
                let b:what = nr2char(getchar())
                let s:cmd .= b:what
            endif
            exec "normal v".b:gzWhere.b:what
            exec "normal g".l:hz
        endif

        "自身の終了時にイベントで-1されるため、2を設定
        let b:dotReady = 2 | redraw!
    endfu "}}}
"}}}

"共通処理{{{
    "現在の囲い文字の入力を受け付けて、その範囲を取得する
    fu! s:getSurround(dotRep) abort "{{{

        "囲う対象を取得
        if a:dotRep == v:true
            "ドットリピート時は前回値を使用
            let what = b:what
        else
            redraw! | echo s:cmd
            let what = nr2char(getchar())
            let b:what = what
            let s:cmd .= what
        endif

        "すでにビジュアルモードの場合、モードを終了する。(現状はnmapのみなのであり得ない想定)
        if mode() ==? "v"
            exec "normal! " . mode()
        endif

        if what =~ "[[:graph:]]"
            "ASCIIの場合
            "※nomalに!を付けないことで、他で定義されたtext-objectを有効にする

            "内側の範囲を取得
            exec "normal vi".what
            let s:iEnd = getcurpos()    | normal! o
            let s:iStart = getcurpos()  | normal! v


            "外側の範囲を取得
            exec "normal va".what
            let s:aEnd = getcurpos()    | normal! o
            let s:aStart = getcurpos()  | normal! v

            "半角文字としてテキストオブジェクトが見つからなかった場合、全角文字扱いにして再検索させる。
            if s:iStart == s:iEnd
                let what = ToZenkaku(what)
            endif
        endif


        "非ASCII(全角)の場合
        if what !~ "[[:graph:]]"

            "開始位置を取得
            let pairs = s:getPairs(what)
            exec "normal! F". pairs[0]
            let s:aStart = getcurpos() | normal! l
            let s:iStart = getcurpos()

            "終了位置を取得
            exec "normal! f". pairs[1]
            let s:aEnd = getcurpos() | normal! h
            let s:iEnd = getcurpos()
        endif


        "範囲確認用
        "message clear
        "echom s:aStart[2]."-".s:iStart[2]."(".s:iStart[1].") , ".s:iEnd[2]."-".s:aEnd[2]."(".s:iEnd[1].")"
    endf "}}}

    "変更後/追加する囲い文字の入力を受け付ける
    fu! s:getAfter() abort "{{{
        redraw! | echo s:cmd
        let wk = nr2char(getchar())

        if wk == '<'
            "タグの場合は、開始タグ・終了タグ
            let l:tagStart =""
            while wk != '>'
                let l:tagStart .= wk
                redraw! | echo s:cmd.l:tagStart
                let wk = nr2char(getchar())
            endwhile
            let l:tagStart .= wk
            redraw! | echo s:cmd.l:tagStart

            let b:afterStart = l:tagStart
            let b:afterEnd   = substitute(l:tagStart,'<','</','')

        else
            "タグ以外の場合は、対になる記号、または、同一記号
            let pairs = s:getPairs(wk)
            let b:afterStart =pairs[0]
            let b:afterEnd   =pairs[1]
        endif

    endf "}}}

    "囲い文字の組み合わせを取得する
    fu! s:getPairs(surround) "{{{
        let mps_save = &mps
        "アングルブラケットをマッチペアに追加※「<」はタグ入力のトリガーなので、「>」用
        set mps+=<:>

        "全角カッコ系を追加
        set mps+=(:),「:」,{:},[:],【:】,『:』,<:>,≪:≫,《:》

        "マッチペアを検索
        let flgMps = 0
        for mp in split(&mps,',')
            let pairs = split(mp,':')
            if a:surround == pairs[0] || a:surround == pairs[1]
                let flgMps = 1 | break
            endif
        endfor

        "マッチペアを戻す
        let &mps = mps_save

        if flgMps == 0
            "マッチペアに該当しない場合は、同じ文字を設定する。
            let pairs[0] = a:surround
            let pairs[1] = a:surround
        endif

        return pairs

    endf "}}}

    "変更前のテキストオブジェクトの入力を受け付ける
    fu! s:getMotion(dotRep) abort "{{{
        let l:count = ""
        let l:where = ""
        let l:what  = ""

        "モーション文字列を取得
        if a:dotRep == v:true
            "ドットリピート時は前回値を使用
            let l:count = b:count
            let l:where = b:where
            let l:what  = b:what
        else
            redraw! | echo s:cmd
            let wk = nr2char(getchar())
            while wk =~ "[0-9]"
                let l:count .= wk
                redraw! | echo s:cmd.l:count
                let wk = nr2char(getchar())
            endwhile

            if wk =~ "[ia]"
                let l:where = wk
                redraw! | echo s:cmd.l:count.l:where
                let l:what  = nr2char(getchar())
            else
                let l:what = wk
            endif
            redraw! | echo s:cmd.l:count.l:where.l:what

            let b:count = l:count
            let b:where = l:where
            let b:what  = l:what

            let s:cmd  .= l:count.l:where.l:what
        endif

        "モーションの内側の範囲を取得
        exec "normal v".l:count.l:where.l:what
        let s:iEnd = getcurpos()    | normal! o
        let s:iStart = getcurpos()  | normal! v

    endf '}}}
"}}}

"ドットリピート関連{{{
    "リピート
    fu! s:dotRepeat() "{{{
        if exists("b:dotReady") && b:dotReady != 0
            silent exec "call <SID>".b:dotRepeatFunc."(".v:true.")" | redraw!
        else
            normal! .
        endif
    endf "}}}

    "アンドゥ後のリピートに備える
    fu! s:undoRepeat(cmdUndo) "{{{
        if exists("b:dotReady") && b:dotReady != 0
            let b:dotReady = 2
        endif
        exec "normal! ".a:cmdUndo
    endf "}}}

    "リピート時の関数名を取得する
    fu! s:funcName(sfile) "{{{
        return substitute(a:sfile,".*_","","")
    endf "}}}
"}}}

"互換性オプションを復元
let &cpo = s:save_cpo

"test
"<a>'(hoge hoge )'</a>
"<a>'(hoge hoge )'</a>
"<a>'(hoge hoge )'</a>

改善後に気が付く問題点

「サラウンドオブジェクトだけじゃなくて、テキストオブジェクトも対象にする」という事しか考えていなかった私は気が付いていませんでした。

このgzgZを、オペレータとしてみた場合、カーソル移動が利かないのです。
こんなものは、もはやとんだ欠陥オペレータと言っていいでしょう。

一人で作っていても、これほど不具合てんこ盛りやなので、プラグインとして公開し多数の目にさらされ、メンテナンスの責務を負うということは非常に恐ろしいことであるな、と改めて思いました。

稚拙な学習過程として、こうしてダラダラとした記事に書いてあげるくらいの恥をかいている方が気楽でよいです。

一通り機能がそろったかも版

gzsgZsgzigZigzagZa」と6つも定義されていたマッピングを「gzgZ」の二つにまとめました。
とってもスッキリしましたね。

後は、自前で入力を受け持っていた箇所で、ESCを押して中断したいという場合に、一発で抜けられるようにしたつもりです。

getMotion()関数は、自前でテキストオブジェクトを受け付けるための関数でしたが、もともとySurround()関数の中から呼び出していました。囲われる範囲の2iwとかapとかを受け付ける関数です。ちなみに現状ではi2wとかできないのが課題です。修正の目途はたっていますが、まだ未着手です。忘れそうなのでここにもTODOとして書いておきましょう。⇒omap_test9.vimで対応しました。

今回は、getMotion()関数をgzOperator()関数からも呼び出すように修正を加えました。
gzOperator()では、sを受け取った場合のサラウンドオブジェクトに対する処理と、それ以外を受け取った場合にはテキストオブジェクトまたはカーソル移動として処理させる必要があります。

ySurround()では、もうsまで入力されてしまっていて、次にiaが入力される想定なのですが、gzOperator()では、ia(やそれ以外のカーソル移動)が入力された後に、テキストオブジェクトの残りを受け取る必要があります。
なので、そのまま呼び出すわけにはいきませんでした。若干ソースが汚くなった気がします。即興スクリプトらしいっちゃらしいですが。

omap_test8.vim
" vi: set et ts=4 sts=4 sw=4 fdm=marker :

scriptencoding utf-8

"二重ロード避け
if exists("g:loaded_omap_test8")
  finish
endif
let g:loaded_omap_test8 = 1

"互換性オプションを退避して規定値に設定
let s:save_cpo = &cpo
set cpo&vim

"マッピング{{{
    "変更、削除、追加{{{
    nnoremap <silent> cs :<C-U>call <SID>cSurround(v:false)<CR>
    nnoremap <silent> ds :<C-U>call <SID>dSurround(v:false)<CR>
    nnoremap <silent> ys :<C-U>call <SID>ySurround(v:false)<CR>
    "}}}

    "大文字小文字を変える。※タグ以外に意味はない(大文字にすることはあまり無いだろうけどセットで){{{
    nnoremap <silent> gus :<C-U>call <SID>guSurround(v:false,'u')<CR>
    nnoremap <silent> gUs :<C-U>call <SID>guSurround(v:false,'U')<CR>
    nnoremap <silent> g~s :<C-U>call <SID>guSurround(v:false,'~')<CR>
    "}}}

    "全角半角を変える(z半角,Z全角) ※タグを全角にすることはできるが(しないと思うが)、半角に戻すことはできない。{{{
    nnoremap <silent> gz :<C-U>call <SID>gzOperator(v:false,'z')<CR>
    nnoremap <silent> gZ :<C-U>call <SID>gzOperator(v:false,'Z')<CR>

    "hz_ja.vim が必要(gHLとgZLを使用){{{
        "gHL可能な文字を全て半角に変換する
        "gZL 可能な文字を全て全角に変換する
        "gHA ASCII文字を全て半角に変換する
        "gHH ASCII文字を全て半角に変換する
        "gZA ASCII文字を全て全角に変換する
        "gHM 記号を全て半角に変換する
        "gZM 記号を全て全角に変換する
        "gHW 英数字を全て半角に変換する
        "gZW 英数字を全て全角に変換する
        "gHJ カタカナを全て半角に変換する
        "gZJ カタカナを全て全角に変換する
        "gZZ カタカナを全て全角に変換する
    "}}}
    "}}}

    "リピート対応{{{
    nmap . :<C-U>call <SID>dotRepeat()<CR>
    nmap u :<C-U>call <SID>undoRepeat("u")<CR>
    nmap U :<C-U>call <SID>undoRepeat("U")<CR>
    "}}}

"}}}

"イベント{{{
    au TextChanged,TextChangedI,TextChangedP,TextYankPost * call <SID>textChanged()
    fu! s:textChanged() "{{{
        if exists("b:dotReady")
            "テキストが変更されたときに、-1する。
            let b:dotReady = ( b:dotReady==0 ? 0 : eval(b:dotReady - 1) )
        endif
    endf "}}}
"}}}

"オペレータ主処理{{{
    fu! s:cSurround(dotRep) abort "{{{
        "ドットリピート用に関数名を保持
        let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
        let s:cmd = "cs"

        "囲い部分の範囲を取得
        "call s:getSurround(a:dotRep)
        if s:getSurround(a:dotRep)
            return
        endif

        "ドットリピート時は再取得しない
        if a:dotRep == v:false
            "変更後の囲い文字を取得
            call s:getAfter()
        endif

        "囲い終わり箇所の置き換え
        call setpos('.',s:iEnd) | normal! lv
        call setpos('.',s:aEnd)
        exec "normal! c".b:afterEnd

        "囲い始め箇所の置き換え
        call setpos('.',s:aStart) | normal! v
        call setpos('.',s:iStart) | normal! h
        exec "normal! c".b:afterStart

        "自身の終了時にイベントで-1されるため、2を設定
        let b:dotReady = 2 | redraw!
    endf "}}}

    fu! s:dSurround(dotRep) abort "{{{
        "ドットリピート用に関数名を保持
        let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
        let s:cmd = "ds"

        "囲い部分の範囲を取得
        "call s:getSurround(a:dotRep)
        if s:getSurround(a:dotRep)
            return
        endif

        "囲い終わり箇所を削除
        call setpos('.',s:iEnd) | normal! lv
        call setpos('.',s:aEnd) | normal! d

        "囲い始め箇所を削除
        call setpos('.',s:aStart) | normal! v
        call setpos('.',s:iStart) | normal! hd

        "自身の終了時にイベントで-1されるため、2を設定
        let b:dotReady = 2 | redraw!
    endfu "}}}

    fu! s:ySurround(dotRep) abort "{{{
        "ドットリピート用に関数名を保持
        let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
        let s:cmd = "ys"

        "囲う部分のテキストオブジェクト部分を取得
        if s:getMotion(a:dotRep)
            return
        endif

        "ドットリピート時は再取得しない
        if a:dotRep == v:false
            "追加する囲い文字を取得
            call s:getAfter()
        endif

        "囲いを追加
        call setpos('.',s:iEnd)     | exec "normal! a".b:afterEnd
        call setpos('.',s:iStart)   | exec "normal! i".b:afterStart

        "自身の終了時にイベントで-1されるため、2を設定
        let b:dotReady = 2 | redraw!
    endf "}}}

    fu! s:guSurround(dotRep,...) abort "{{{
        "ドットリピート用に関数名を保持
        let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
        if a:0
            let b:case = a:1
        endif
        let s:cmd = "g".b:case."s"

        "囲い部分の範囲を取得
        "call s:getSurround(a:dotRep)
        if s:getSurround(a:dotRep)
            return
        endif

        "囲い終わり箇所を処理
        call setpos('.',s:iEnd) | normal! lv
        exec "call setpos('.',s:aEnd) | normal! g".b:case

        "囲い始め箇所を処理
        call setpos('.',s:aStart) | normal! v
        exec "call setpos('.',s:iStart) | normal! hg".b:case

        "自身の終了時にイベントで-1されるため、2を設定
        let b:dotReady = 2 | redraw!
    endfu "}}}

    fu! s:gzOperator(dotRep,...) abort "{{{
        "ドットリピート用に関数名を保持
        let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
        if a:0
            let b:case = a:1
        endif
        let s:cmd = "g".b:case
        if a:dotRep == v:false
            redraw! | echo s:cmd
            let b:gzWhere = nr2char(getchar())
        endif

        if b:case ==# 'z'
            let l:hz = 'HL'
        else
            let l:hz = 'ZL'
        endif

        if b:gzWhere == 's'
            let s:cmd .= b:gzWhere
            "囲い部分の範囲を取得
            if s:getSurround(a:dotRep)
                return
            endif

            "囲い終わり箇所を処理
            call setpos('.',s:iEnd) | normal! lv
            exec "call setpos('.',s:aEnd) | normal g".l:hz

            "囲い始め箇所を処理
            call setpos('.',s:aStart) | normal! v
            exec "call setpos('.',s:iStart) | normal hg".l:hz
        else
            if s:getMotion(a:dotRep,b:gzWhere)
                return
            endif
            call setpos('.',s:iStart) | normal! v
            exec "call setpos('.',s:iEnd) | normal g".l:hz
        endif

        "自身の終了時にイベントで-1されるため、2を設定
        let b:dotReady = 2 | redraw!
    endfu "}}}
"}}}

"共通処理{{{
    "現在の囲い文字の入力を受け付けて、その範囲を取得する
    fu! s:getSurround(dotRep) abort "{{{

        "囲う対象を取得
        if a:dotRep == v:true
            "ドットリピート時は前回値を使用
            let what = b:what
        else
            redraw! | echo s:cmd
            let what = nr2char(getchar())
            if what !~ '[[:graph:]]'
                redraw! | return 1
            endif
            let b:what = what
            let s:cmd .= what
        endif

        "すでにビジュアルモードの場合、モードを終了する。(現状はnmapのみなのであり得ない想定)
        if mode() ==? "v"
            exec "normal! " . mode()
        endif

        if what =~ "[[:graph:]]"
            "ASCIIの場合
            "※nomalに!を付けないことで、他で定義されたtext-objectを有効にする

            "内側の範囲を取得
            exec "normal vi".what
            let s:iEnd = getcurpos()    | normal! o
            let s:iStart = getcurpos()  | normal! v

            "外側の範囲を取得
            exec "normal va".what
            let s:aEnd = getcurpos()    | normal! o
            let s:aStart = getcurpos()  | normal! v

            "半角文字としてテキストオブジェクトが見つからなかった場合、全角文字扱いにして再検索させる。
            if s:iStart == s:iEnd
                let what = ToZenkaku(what)
            endif
        endif

        "非ASCII(全角)の場合
        if what !~ "[[:graph:]]"

            "開始位置を取得
            let pairs = s:getPairs(what)
            exec "normal! F". pairs[0]
            let s:aStart = getcurpos() | normal! l
            let s:iStart = getcurpos()

            "終了位置を取得
            exec "normal! f". pairs[1]
            let s:aEnd = getcurpos() | normal! h
            let s:iEnd = getcurpos()
        endif

        "範囲確認用
        "message clear
        "echom s:aStart[2]."-".s:iStart[2]."(".s:iStart[1].") , ".s:iEnd[2]."-".s:aEnd[2]."(".s:iEnd[1].")"

        return 0
    endf "}}}

    "変更後/追加する囲い文字の入力を受け付ける
    fu! s:getAfter() abort "{{{
        redraw! | echo s:cmd
        let wk = nr2char(getchar())

        if wk == '<'
            "タグの場合は、開始タグ・終了タグ
            let l:tagStart =""
            while wk != '>'
                let l:tagStart .= wk
                redraw! | echo s:cmd.l:tagStart
                let wk = nr2char(getchar())
            endwhile
            let l:tagStart .= wk
            redraw! | echo s:cmd.l:tagStart

            let b:afterStart = l:tagStart
            let b:afterEnd   = substitute(l:tagStart,'<','</','')

        else
            "タグ以外の場合は、対になる記号、または、同一記号
            let pairs = s:getPairs(wk)
            let b:afterStart =pairs[0]
            let b:afterEnd   =pairs[1]
        endif

    endf "}}}

    "囲い文字の組み合わせを取得する
    fu! s:getPairs(surround) "{{{
        let mps_save = &mps
        "アングルブラケットをマッチペアに追加※「<」はタグ入力のトリガーなので、「>」用
        set mps+=<:>

        "全角カッコ系を追加
        set mps+=(:),「:」,{:},[:],【:】,『:』,<:>,≪:≫,《:》

        "マッチペアを検索
        let flgMps = 0
        for mp in split(&mps,',')
            let pairs = split(mp,':')
            if a:surround == pairs[0] || a:surround == pairs[1]
                let flgMps = 1 | break
            endif
        endfor

        "マッチペアを戻す
        let &mps = mps_save

        if flgMps == 0
            "マッチペアに該当しない場合は、同じ文字を設定する。
            let pairs[0] = a:surround
            let pairs[1] = a:surround
        endif

        return pairs

    endf "}}}

    "変更前のテキストオブジェクトの入力を受け付ける
    fu! s:getMotion(dotRep,...) abort "{{{
        let l:count = ""
        let l:where = ""
        let l:what  = ""

        "モーション文字列を取得
        if a:dotRep == v:true
            "ドットリピート時は前回値を使用
            let l:count = b:count
            let l:where = b:where
            let l:what  = b:what
        else
            redraw! | echo s:cmd
            if a:0
                let wk = a:1
            else
                let wk = nr2char(getchar())
            endif
            if wk !~ '[[:graph:]]'
                redraw! | return 1
            endif

            while wk =~ "[0-9]"
                let l:count .= wk
                redraw! | echo s:cmd.l:count
                let wk = nr2char(getchar())
                if wk !~ '[[:graph:]]'
                    redraw! | return 1
                endif
            endwhile

            if wk =~ "[ia]"
                let l:where = wk
                redraw! | echo s:cmd.l:count.l:where
                let wk  = nr2char(getchar())
                if wk !~ '[[:graph:]]'
                    redraw! | return 1
                endif
            endif
            let l:what = wk

            redraw! | echo s:cmd.l:count.l:where.l:what

            let b:count = l:count
            let b:where = l:where
            let b:what  = l:what

            let s:cmd  .= l:count.l:where.l:what
        endif

        "モーションの内側の範囲を取得
        exec "normal v".l:count.l:where.l:what
        let s:iEnd = getcurpos()    | normal! o
        let s:iStart = getcurpos()  | normal! v

        return 0
    endf '}}}
"}}}

"ドットリピート関連{{{
    "リピート
    fu! s:dotRepeat() "{{{
        if exists("b:dotReady") && b:dotReady != 0
            silent exec "call <SID>".b:dotRepeatFunc."(".v:true.")" | redraw!
        else
            normal! .
        endif
    endf "}}}

    "アンドゥ後のリピートに備える
    fu! s:undoRepeat(cmdUndo) "{{{
        if exists("b:dotReady") && b:dotReady != 0
            let b:dotReady = 2
        endif
        exec "normal! ".a:cmdUndo
    endf "}}}

    "リピート時の関数名を取得する
    fu! s:funcName(sfile) "{{{
        return substitute(a:sfile,".*_","","")
    endf "}}}
"}}}

"互換性オプションを復元
let &cpo = s:save_cpo

"test
"<a>'(hoge hoge )'</a>
"<a>'(hoge hoge )'</a>
"<a>'(hoge hoge )'</a>

さらなる改善点

今思いついている改善点は以下のようなものです。

  • テキストオブジェクトの受付時のcount指定位置が先頭にしか対処していない。
  • ドットリピート時にcountを付けた場合、countだけ差し替えて、リピートさせたい。

    ys2iw"とした後に、3.ってやったら、ys3iwが発動するような感じです。

どちらもやり方の想像は付いているので、対応したらまた記事を差し替えるかもしれません。
やったらやったで、また何か気になるところが出てきそうなのでキリがない感じですね。

さらに改善してみた。

上で上げた2点と、ヴィジュアルモードのSに対応しました。

ドットリピート時のcount再指定はなかなか面白いですね。
gZwとやって、1単語分を全角にした後に、2.とすると、2単語分が全角になります。
ysi2w"とやって、2単語を"括りにした後に、後に、1.とすると1単語分だけが"括りになります。

ついでに、上記はys2iw"とやった後でも同じです。

ヴィジュアルモードでのS時のドットリピートでは、count指定は意味がありません。
ですが、.押下時には、前回S"実行時の選択範囲と同じ相対範囲を対象に"で括る、という動きをします。まぁ、何か使い道があると思いましょう。

omap_test9.vim
" vi: set et ts=4 sts=4 sw=4 fdm=marker :

scriptencoding utf-8

"二重ロード避け
if exists("g:loaded_omap_test9")
  finish
endif
let g:loaded_omap_test9 = 1

"互換性オプションを退避して規定値に設定
let s:save_cpo = &cpo
set cpo&vim

"マッピング{{{
    "変更、削除、追加{{{
    nnoremap <silent> cs :<C-U>call <SID>cSurround(v:false)<CR>
    nnoremap <silent> ds :<C-U>call <SID>dSurround(v:false)<CR>
    nnoremap <silent> ys :<C-U>call <SID>ySurround(v:false)<CR>
    vnoremap <silent> S :<C-U>call <SID>sSurround(v:false)<CR>
    "}}}

    "大文字小文字を変える。※タグ以外に意味はない(大文字にすることはあまり無いだろうけどセットで){{{
    nnoremap <silent> gus :<C-U>call <SID>guSurround(v:false,'u')<CR>
    nnoremap <silent> gUs :<C-U>call <SID>guSurround(v:false,'U')<CR>
    nnoremap <silent> g~s :<C-U>call <SID>guSurround(v:false,'~')<CR>
    "}}}

    "全角半角を変える(z半角,Z全角) ※タグを全角にすることはできるが(しないと思うが)、半角に戻すことはできない。{{{
    nnoremap <silent> gz :<C-U>call <SID>gzOperator(v:false,'z')<CR>
    nnoremap <silent> gZ :<C-U>call <SID>gzOperator(v:false,'Z')<CR>

    "hz_ja.vim が必要(gHLとgZLを使用){{{
        "gHL可能な文字を全て半角に変換する
        "gZL 可能な文字を全て全角に変換する
        "gHA ASCII文字を全て半角に変換する
        "gHH ASCII文字を全て半角に変換する
        "gZA ASCII文字を全て全角に変換する
        "gHM 記号を全て半角に変換する
        "gZM 記号を全て全角に変換する
        "gHW 英数字を全て半角に変換する
        "gZW 英数字を全て全角に変換する
        "gHJ カタカナを全て半角に変換する
        "gZJ カタカナを全て全角に変換する
        "gZZ カタカナを全て全角に変換する
    "}}}
    "}}}

    "リピート対応{{{
    nmap . :<C-U>call <SID>dotRepeat()<CR>
    nmap u :<C-U>call <SID>undoRepeat("u")<CR>
    nmap U :<C-U>call <SID>undoRepeat("U")<CR>
    "}}}

"}}}

"イベント{{{
    au TextChanged,TextChangedI,TextChangedP,TextYankPost * call <SID>textChanged()
    fu! s:textChanged() "{{{
        if exists("b:dotReady")
            "テキストが変更されたときに、-1する。
            let b:dotReady = ( b:dotReady==0 ? 0 : eval(b:dotReady - 1) )
        endif
    endf "}}}
"}}}

"オペレータ主処理{{{
    fu! s:cSurround(dotRep) abort "{{{
        "ドットリピート用に関数名を保持
        let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
        let s:cmd = "cs"

        "囲い部分の範囲を取得
        "call s:getSurround(a:dotRep)
        if s:getSurround(a:dotRep)
            return
        endif

        "ドットリピート時は再取得しない
        if a:dotRep == v:false
            "変更後の囲い文字を取得
            call s:getAfter()
        endif

        "囲い終わり箇所の置き換え
        call setpos('.',s:iEnd) | normal! lv
        call setpos('.',s:aEnd)
        exec "normal! c".b:afterEnd

        "囲い始め箇所の置き換え
        call setpos('.',s:aStart) | normal! v
        call setpos('.',s:iStart) | normal! h
        exec "normal! c".b:afterStart

        "自身の終了時にイベントで-1されるため、2を設定
        let b:dotReady = 2 | redraw!
    endf "}}}

    fu! s:dSurround(dotRep) abort "{{{
        "ドットリピート用に関数名を保持
        let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
        let s:cmd = "ds"

        "囲い部分の範囲を取得
        "call s:getSurround(a:dotRep)
        if s:getSurround(a:dotRep)
            return
        endif

        "囲い終わり箇所を削除
        call setpos('.',s:iEnd) | normal! lv
        call setpos('.',s:aEnd) | normal! d

        "囲い始め箇所を削除
        call setpos('.',s:aStart) | normal! v
        call setpos('.',s:iStart) | normal! hd

        "自身の終了時にイベントで-1されるため、2を設定
        let b:dotReady = 2 | redraw!
    endfu "}}}

    fu! s:ySurround(dotRep) abort "{{{
        "ドットリピート用に関数名を保持
        let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
        let s:cmd = "ys"

        "囲う部分のテキストオブジェクト部分を取得
        if s:getMotion(a:dotRep)
            return
        endif

        "ドットリピート時は再取得しない
        if a:dotRep == v:false
            "追加する囲い文字を取得
            call s:getAfter()
        endif

        "囲いを追加
        call setpos('.',s:iEnd)     | exec "normal! a".b:afterEnd
        call setpos('.',s:iStart)   | exec "normal! i".b:afterStart

        "自身の終了時にイベントで-1されるため、2を設定
        let b:dotReady = 2 | redraw!
    endf "}}}

    fu! s:sSurround(dotRep) abort "{{{
        "ドットリピート用に関数名を保持
        let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
        let s:cmd = "S"

        if a:dotRep == v:false
            "再選択
            normal! gv
            "囲い範囲を取得
            let iEnd = getcurpos()    | normal! o
            let iStart = getcurpos()  | normal! v
            if iStart[1] == iEnd[1]
                "開始終了位置が、同一行の場合
                if iEnd[2] < iStart[2]
                    "開始終了列が逆転している場合
                    let wk = iStart | let iStart = iEnd | let iEnd = wk
                endif
            elseif iEnd[1] < iStart[1]
                "開始終了行が逆転している場合
                let wk = iStart | let iStart = iEnd | let iEnd = wk
            endif
            "相対位置を記憶
            let b:vRight = iEnd[2] - iStart[2]
            let b:vDown = iEnd[1] - iStart[1]
            "開始位置にカーソルを合わせる
            call setpos('.',iStart)

            "追加する囲い文字を取得
            call s:getAfter()
        endif

        "messages clear
        "echom iStart
        "echom iEnd
        "echom b:vRight . " " . b:vDown

        "囲い開始
        exec "normal! i".b:afterStart

        "カーソル移動
        if 0 < b:vDown
            exec "normal".eval(b:vDown)."j"
        endif
        if 0 < b:vRight
            exec "normal".eval(b:vRight+1)."l"
        endif

        "囲い終わり
        exec "normal! a".b:afterEnd

        "自身の終了時にイベントで-1されるため、2を設定
        let b:dotReady = 2 | redraw!
    endf "}}}

    fu! s:guSurround(dotRep,...) abort "{{{
        "ドットリピート用に関数名を保持
        let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
        if a:0
            let b:case = a:1
        endif
        let s:cmd = "g".b:case."s"

        "囲い部分の範囲を取得
        "call s:getSurround(a:dotRep)
        if s:getSurround(a:dotRep)
            return
        endif

        "囲い終わり箇所を処理
        call setpos('.',s:iEnd) | normal! lv
        exec "call setpos('.',s:aEnd) | normal! g".b:case

        "囲い始め箇所を処理
        call setpos('.',s:aStart) | normal! v
        exec "call setpos('.',s:iStart) | normal! hg".b:case

        "自身の終了時にイベントで-1されるため、2を設定
        let b:dotReady = 2 | redraw!
    endfu "}}}

    fu! s:gzOperator(dotRep,...) abort "{{{
        "ドットリピート用に関数名を保持
        let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
        if a:0
            let b:case = a:1
        endif
        let s:cmd = "g".b:case
        if a:dotRep == v:false
            redraw! | echo s:cmd
            let b:gzWhere = nr2char(getchar())
        endif

        if b:case ==# 'z'
            let l:hz = 'HL'
        else
            let l:hz = 'ZL'
        endif

        if b:gzWhere == 's'
            let s:cmd .= b:gzWhere
            "囲い部分の範囲を取得
            if s:getSurround(a:dotRep)
                return
            endif

            "囲い終わり箇所を処理
            call setpos('.',s:iEnd) | normal! lv
            exec "call setpos('.',s:aEnd) | normal g".l:hz

            "囲い始め箇所を処理
            call setpos('.',s:aStart) | normal! v
            exec "call setpos('.',s:iStart) | normal hg".l:hz
        else
            if s:getMotion(a:dotRep,b:gzWhere)
                return
            endif
            call setpos('.',s:iStart) | normal! v
            exec "call setpos('.',s:iEnd) | normal g".l:hz
        endif

        "自身の終了時にイベントで-1されるため、2を設定
        let b:dotReady = 2 | redraw!
    endfu "}}}
"}}}

"共通処理{{{
    "現在の囲い文字の入力を受け付けて、その範囲を取得する
    fu! s:getSurround(dotRep) abort "{{{

        "囲う対象を取得
        if a:dotRep == v:true
            "ドットリピート時は前回値を使用
            let what = b:what
        else
            redraw! | echo s:cmd
            let what = nr2char(getchar())
            if what !~ '[[:graph:]]'
                redraw! | return 1
            endif
            let b:what = what
            let s:cmd .= what
        endif

        "すでにビジュアルモードの場合、モードを終了する。(現状はnmapのみなのであり得ない想定)
        if mode() ==? "v"
            exec "normal! " . mode()
        endif

        if what =~ "[[:graph:]]"
            "ASCIIの場合
            "※nomalに!を付けないことで、他で定義されたtext-objectを有効にする

            "内側の範囲を取得
            exec "normal vi".what
            let s:iEnd = getcurpos()    | normal! o
            let s:iStart = getcurpos()  | normal! v

            "外側の範囲を取得
            exec "normal va".what
            let s:aEnd = getcurpos()    | normal! o
            let s:aStart = getcurpos()  | normal! v

            "半角文字としてテキストオブジェクトが見つからなかった場合、全角文字扱いにして再検索させる。
            if s:iStart == s:iEnd
                let what = ToZenkaku(what)
            endif
        endif

        "非ASCII(全角)の場合
        if what !~ "[[:graph:]]"

            "開始位置を取得
            let pairs = s:getPairs(what)
            exec "normal! F". pairs[0]
            let s:aStart = getcurpos() | normal! l
            let s:iStart = getcurpos()

            "終了位置を取得
            exec "normal! f". pairs[1]
            let s:aEnd = getcurpos() | normal! h
            let s:iEnd = getcurpos()
        endif

        "範囲確認用
        "message clear
        "echom s:aStart[2]."-".s:iStart[2]."(".s:iStart[1].") , ".s:iEnd[2]."-".s:aEnd[2]."(".s:iEnd[1].")"

        return 0
    endf "}}}

    "変更後/追加する囲い文字の入力を受け付ける
    fu! s:getAfter() abort "{{{
        redraw! | echo s:cmd
        let wk = nr2char(getchar())

        if wk == '<'
            "タグの場合は、開始タグ・終了タグ
            let l:tagStart =""
            while wk != '>'
                let l:tagStart .= wk
                redraw! | echo s:cmd.l:tagStart
                let wk = nr2char(getchar())
            endwhile
            let l:tagStart .= wk
            redraw! | echo s:cmd.l:tagStart

            let b:afterStart = l:tagStart
            let b:afterEnd   = substitute(l:tagStart,'<','</','')

        else
            "タグ以外の場合は、対になる記号、または、同一記号
            let pairs = s:getPairs(wk)
            let b:afterStart =pairs[0]
            let b:afterEnd   =pairs[1]
        endif

    endf "}}}

    "囲い文字の組み合わせを取得する
    fu! s:getPairs(surround) "{{{
        let mps_save = &mps
        "アングルブラケットをマッチペアに追加※「<」はタグ入力のトリガーなので、「>」用
        set mps+=<:>

        "全角カッコ系を追加
        set mps+=(:),「:」,{:},[:],【:】,『:』,<:>,≪:≫,《:》

        "マッチペアを検索
        let flgMps = 0
        for mp in split(&mps,',')
            let pairs = split(mp,':')
            if a:surround == pairs[0] || a:surround == pairs[1]
                let flgMps = 1 | break
            endif
        endfor

        "マッチペアを戻す
        let &mps = mps_save

        if flgMps == 0
            "マッチペアに該当しない場合は、同じ文字を設定する。
            let pairs[0] = a:surround
            let pairs[1] = a:surround
        endif

        return pairs

    endf "}}}

    "変更前のテキストオブジェクトの入力を受け付ける
    fu! s:getMotion(dotRep,...) abort "{{{
        let l:count = ""
        let l:where = ""
        let l:what  = ""

        "モーション文字列を取得
        if a:dotRep == v:true
            "ドットリピート時は前回値を使用
            let l:count = b:count
            let l:where = b:where
            let l:what  = b:what
        else
            "リピート時以外は、テキストオブジェクトの入力を受け付ける
            redraw! | echo s:cmd
            if a:0
                "可変引数あり時は、すでに1文字目が入力されているとみなす
                let wk = a:1
            else
                let wk = nr2char(getchar())
            endif
            "無効な文字以外は終了
            if wk !~ '[[:graph:]]'
                redraw! | return 1
            endif

            while wk =~ "[0-9]"
                "数字が入力され続ける間、[count]に保持
                let l:count .= wk
                let s:cmd .= l:count | redraw! | echo s:cmd
                let wk = nr2char(getchar())
                if wk !~ '[[:graph:]]'
                    redraw! | return 1
                endif
            endwhile

            if wk =~ "[ia]"
                "テキストオブジェクトの場合
                let l:where = wk
                let s:cmd .= l:where | redraw! | echo s:cmd
                "もう1文字受け取ってテキストオブジェクトを完成させる
                let wk  = nr2char(getchar())
                if wk =~ "[0-9]"
                    "数字が入力された場合
                    if l:count == ""
                        "以前にcountが入力されていない場合、[count]として取得
                        while wk =~ "[0-9]"
                            let l:count .= wk
                            let s:cmd .= l:count | redraw! | echo s:cmd
                            let wk = nr2char(getchar())
                            if wk !~ '[[:graph:]]'
                                redraw! | return 1
                            endif
                        endwhile
                    else
                        "以前にcounttが入力されていた場合は、処理中断
                        redraw! | return 1
                    endif
                endif
                if wk !~ '[[:graph:]]'
                    redraw! | return 1
                endif
            endif
            "受け取った文字を連結(カーソル移動の場合も含む)
            let l:what = wk
            let s:cmd .= l:what | redraw! | echo s:cmd

            let b:count = l:count
            let b:where = l:where
            let b:what  = l:what

        endif

        "モーションの内側の範囲を取得
        exec "normal v".l:count.l:where.l:what
        let s:iEnd = getcurpos()    | normal! o
        let s:iStart = getcurpos()  | normal! v

        return 0
    endf "}}}
"}}}

"ドットリピート関連{{{
    "リピート
    fu! s:dotRepeat() "{{{

        if exists("b:dotReady") && b:dotReady != 0
            let b:count = v:count
            silent exec "call <SID>".b:dotRepeatFunc."(".v:true.")" | redraw!
        else
            normal! .
        endif
    endf "}}}

    "アンドゥ後のリピートに備える
    fu! s:undoRepeat(cmdUndo) "{{{
        if exists("b:dotReady") && b:dotReady != 0
            let b:dotReady = 2
        endif
        exec "normal! ".a:cmdUndo
    endf "}}}

    "リピート時の関数名を取得する
    fu! s:funcName(sfile) "{{{
        return substitute(a:sfile,".*_","","")
    endf "}}}
"}}}

"互換性オプションを復元
let &cpo = s:save_cpo

"test
"<a>'(hoge hoge )'</a>
"<a>'(hoge hoge )'</a>
"<a>'(hoge hoge )'</a>

問題点・改善点

修正すれば、やはり問題点も出てくるものです。

  • ysil{として、行単位に括り文字を追加した場合など、場合によってはオートインデントがかかってしまう。(既存)
  • ドットリピート時にcount指定をしないと、まともに動作しない。
  • {visual}Sで、開始行のカラム位置よりも、終了行のカラム開始位置が左側に居た場合、ドットリピート時の範囲がおかしくなる。

修正した箇所ごとに、何らかの不具合を盛り込んでいくスタイル。

こうなってくると、countの受付を2iwだけではなくi2wも可能にした箇所にも何か問題が潜んでいる気がしてなりません。

修正版

上記の問題点全部対応しました。

ついでに、以下の改善を行いました。(また不具合が混入している可能性あり)

  • 変更後のサラウンドとしてb()囲いになったり、B{}囲いになったりするように。

    csbBとかcsBbとか出来るようなった。
  • ys時に、続けてsと打った場合、行単位として扱うようにした。yss"で、行単位に括ってくれるようになった。

今回対応した問題点の、{visual}Sのドットリピート時のカラム位置についてですが、標準オペレータの{visual}u等の場合は、行数は.を押した箇所から、相対的に下方向へ対象にしてくれますが、最後の行のカラム位置は、最初に実行した時から変わらないようです。
また、vk等で、上方向に選択してuした後でも、ドットリピート時には、処理対象になった行数分を下方向に対象にするようです。
vbで左方向に選択してuした後の.でも、右方向へ同一文字数分が処理対象になります。
上向きや左向きに選択したんだから、リピート時も上方向や左向きに対象にされるのかと思いましたが、そんなことはありませんでした。上下方向については、標準の振る舞いに従っている形になります。

ただし、{visual}Sは、カラム位置についても、相対的に左右方向へ終了位置をズラす様にしてあるので、なんだか小汚い実装になっています。

omap_test10.vim
" vi: set et ts=4 sts=4 sw=4 fdm=marker :

scriptencoding utf-8

"二重ロード避け
if exists("g:loaded_omap_test10")
  finish
endif
let g:loaded_omap_test10 = 1

"互換性オプションを退避して規定値に設定
let s:save_cpo = &cpo
set cpo&vim

"マッピング{{{
    "変更、削除、追加{{{
    nnoremap <silent> cs :<C-U>call <SID>cSurround(v:false)<CR>
    nnoremap <silent> ds :<C-U>call <SID>dSurround(v:false)<CR>
    nnoremap <silent> ys :<C-U>call <SID>ySurround(v:false)<CR>
    vnoremap <silent> S :<C-U>call <SID>sSurround(v:false)<CR>
    "}}}

    "大文字小文字を変える。※タグ以外に意味はない(大文字にすることはあまり無いだろうけどセットで){{{
    nnoremap <silent> gus :<C-U>call <SID>guSurround(v:false,'u')<CR>
    nnoremap <silent> gUs :<C-U>call <SID>guSurround(v:false,'U')<CR>
    nnoremap <silent> g~s :<C-U>call <SID>guSurround(v:false,'~')<CR>
    "}}}

    "全角半角を変える(z半角,Z全角) ※タグを全角にすることはできるが(しないと思うが)、半角に戻すことはできない。{{{
    nnoremap <silent> gz :<C-U>call <SID>gzOperator(v:false,'z')<CR>
    nnoremap <silent> gZ :<C-U>call <SID>gzOperator(v:false,'Z')<CR>

    "hz_ja.vim が必要(gHLとgZLを使用){{{
        "gHL可能な文字を全て半角に変換する
        "gZL 可能な文字を全て全角に変換する
        "gHA ASCII文字を全て半角に変換する
        "gHH ASCII文字を全て半角に変換する
        "gZA ASCII文字を全て全角に変換する
        "gHM 記号を全て半角に変換する
        "gZM 記号を全て全角に変換する
        "gHW 英数字を全て半角に変換する
        "gZW 英数字を全て全角に変換する
        "gHJ カタカナを全て半角に変換する
        "gZJ カタカナを全て全角に変換する
        "gZZ カタカナを全て全角に変換する
    "}}}
    "}}}

    "リピート対応{{{
    nmap . :<C-U>call <SID>dotRepeat()<CR>
    nmap u :<C-U>call <SID>undoRepeat("u")<CR>
    nmap U :<C-U>call <SID>undoRepeat("U")<CR>
    "}}}

"}}}

"イベント{{{
    au TextChanged,TextChangedI,TextChangedP,TextYankPost * call <SID>textChanged()
    fu! s:textChanged() "{{{
        if exists("b:dotReady")
            "テキストが変更されたときに、-1する。
            let b:dotReady = ( b:dotReady==0 ? 0 : eval(b:dotReady - 1) )
        endif
    endf "}}}
"}}}

"オペレータ主処理{{{
    fu! s:cSurround(dotRep) abort "{{{
        "ドットリピート用に関数名を保持
        let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
        let s:cmd = "cs"

        "囲い部分の範囲を取得
        "call s:getSurround(a:dotRep)
        if s:getSurround(a:dotRep)
            return
        endif

        "ドットリピート時は再取得しない
        if a:dotRep == v:false
            "変更後の囲い文字を取得
            call s:getAfter()
        endif

        try
            let ai_save = &ai
            setl noai
            "囲い終わり箇所の置き換え
            call setpos('.',s:iEnd) | normal! lv
            call setpos('.',s:aEnd)
            exec "normal! c".b:afterEnd

            "囲い始め箇所の置き換え
            call setpos('.',s:aStart) | normal! v
            call setpos('.',s:iStart) | normal! h
            exec "normal! c".b:afterStart
        finally
            let &ai = ai_save
        endtry

        "自身の終了時にイベントで-1されるため、2を設定
        let b:dotReady = 2 | redraw!
    endf "}}}

    fu! s:dSurround(dotRep) abort "{{{
        "ドットリピート用に関数名を保持
        let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
        let s:cmd = "ds"

        "囲い部分の範囲を取得
        "call s:getSurround(a:dotRep)
        if s:getSurround(a:dotRep)
            return
        endif

        try
            let ai_save = &ai
            setl noai
            "囲い終わり箇所を削除
            call setpos('.',s:iEnd) | normal! lv
            call setpos('.',s:aEnd) | normal! d

            "囲い始め箇所を削除
            call setpos('.',s:aStart) | normal! v
            call setpos('.',s:iStart) | normal! hd
        finally
            let &ai = ai_save
        endtry

        "自身の終了時にイベントで-1されるため、2を設定
        let b:dotReady = 2 | redraw!
    endfu "}}}

    fu! s:ySurround(dotRep) abort "{{{
        "ドットリピート用に関数名を保持
        let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
        let s:cmd = "ys"

        "囲う部分のテキストオブジェクト部分を取得
        if s:getMotion(a:dotRep)
            return
        endif

        "ドットリピート時は再取得しない
        if a:dotRep == v:false
            "追加する囲い文字を取得
            call s:getAfter()
        endif

        try
            let ai_save = &ai
            setl noai
            "囲いを追加
            call setpos('.',s:iEnd)     | exec "normal! a".b:afterEnd
            call setpos('.',s:iStart)   | exec "normal! i".b:afterStart
        finally
            let &ai = ai_save
        endtry

        "自身の終了時にイベントで-1されるため、2を設定
        let b:dotReady = 2 | redraw!
    endf "}}}

    fu! s:sSurround(dotRep) abort "{{{
        "ドットリピート用に関数名を保持
        let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
        let s:cmd = "S"

        if a:dotRep == v:false
            "再選択
            normal! gv
            "囲い範囲を取得
            let iEnd = getcurpos()    | normal! o
            let iStart = getcurpos()  | normal! v
            if iStart[1] == iEnd[1]
                "開始終了位置が、同一行の場合
                if iEnd[2] < iStart[2]
                    "開始終了列が逆転している場合
                    let wk = iStart | let iStart = iEnd | let iEnd = wk
                endif
            elseif iEnd[1] < iStart[1]
                "開始終了行が逆転している場合
                let wk = iStart | let iStart = iEnd | let iEnd = wk
            endif
            "相対位置を記憶
            let b:vRight = iEnd[2] - iStart[2]
            let b:vDown = iEnd[1] - iStart[1]
            "開始位置にカーソルを合わせる
            call setpos('.',iStart)

            "追加する囲い文字を取得
            call s:getAfter()
        endif

        "messages clear
        "echom iStart
        "echom iEnd
        "echom b:vRight . " " . b:vDown

        try
            let ai_save = &ai
            setl noai
            "囲い開始
            exec "normal! i".b:afterStart

            "カーソル移動
            if 0 < b:vDown
                exec "normal".eval(b:vDown)."j"
                if 0 < b:vRight
                    exec "normal".eval(b:vRight)."l"
                elseif b:vRight < 0
                    exec "normal".eval(b:vRight*-1)."h"
                endif
            else
                if 0 < b:vRight
                    exec "normal".eval(b:vRight+1)."l"
                elseif b:vRight < 0
                    exec "normal".eval((b:vRight*-1)+1)."h"
                endif
            endif

            "囲い終わり
            exec "normal! a".b:afterEnd
        finally
            let &ai = ai_save
        endtry

        "自身の終了時にイベントで-1されるため、2を設定
        let b:dotReady = 2 | redraw!
    endf "}}}

    fu! s:guSurround(dotRep,...) abort "{{{
        "ドットリピート用に関数名を保持
        let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
        if a:0
            let b:case = a:1
        endif
        let s:cmd = "g".b:case."s"

        "囲い部分の範囲を取得
        "call s:getSurround(a:dotRep)
        if s:getSurround(a:dotRep)
            return
        endif

        try
            let ai_save = &ai
            setl noai
            "囲い終わり箇所を処理
            call setpos('.',s:iEnd) | normal! lv
            exec "call setpos('.',s:aEnd) | normal! g".b:case

            "囲い始め箇所を処理
            call setpos('.',s:aStart) | normal! v
            exec "call setpos('.',s:iStart) | normal! hg".b:case
        finally
            let &ai = ai_save
        endtry

        "自身の終了時にイベントで-1されるため、2を設定
        let b:dotReady = 2 | redraw!
    endfu "}}}

    fu! s:gzOperator(dotRep,...) abort "{{{
        "ドットリピート用に関数名を保持
        let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
        if a:0
            let b:case = a:1
        endif
        let s:cmd = "g".b:case
        if a:dotRep == v:false
            redraw! | echo s:cmd
            let b:gzWhere = nr2char(getchar())
        endif

        if b:case ==# 'z'
            let l:hz = 'HL'
        else
            let l:hz = 'ZL'
        endif

        if b:gzWhere == 's'
            let s:cmd .= b:gzWhere
            "囲い部分の範囲を取得
            if s:getSurround(a:dotRep)
                return
            endif

            try
                let ai_save = &ai
                setl noai
                "囲い終わり箇所を処理
                call setpos('.',s:iEnd) | normal! lv
                exec "call setpos('.',s:aEnd) | normal g".l:hz

                "囲い始め箇所を処理
                call setpos('.',s:aStart) | normal! v
                exec "call setpos('.',s:iStart) | normal hg".l:hz
            finally
                let &ai = ai_save
            endtry

        else
            if s:getMotion(a:dotRep,b:gzWhere)
                return
            endif
            try
                let ai_save = &ai
                setl noai
                call setpos('.',s:iStart) | normal! v
                exec "call setpos('.',s:iEnd) | normal g".l:hz
            finally
                let &ai = ai_save
            endtry
        endif

        "自身の終了時にイベントで-1されるため、2を設定
        let b:dotReady = 2 | redraw!
    endfu "}}}
"}}}

"共通処理{{{
    "現在の囲い文字の入力を受け付けて、その範囲を取得する
    fu! s:getSurround(dotRep) abort "{{{

        "囲う対象を取得
        if a:dotRep == v:true
            "ドットリピート時は前回値を使用
            let what = b:what
        else
            redraw! | echo s:cmd
            let what = nr2char(getchar())
            if what !~ '[[:graph:]]'
                redraw! | return 1
            endif
            let b:what = what
            let s:cmd .= what
        endif

        "すでにビジュアルモードの場合、モードを終了する。(現状はnmapのみなのであり得ない想定)
        if mode() ==? "v"
            exec "normal! " . mode()
        endif

        if what =~ "[[:graph:]]"
            "ASCIIの場合
            "※nomalに!を付けないことで、他で定義されたtext-objectを有効にする

            "内側の範囲を取得
            exec "normal vi".what
            let s:iEnd = getcurpos()    | normal! o
            let s:iStart = getcurpos()  | normal! v

            "外側の範囲を取得
            exec "normal va".what
            let s:aEnd = getcurpos()    | normal! o
            let s:aStart = getcurpos()  | normal! v

            "半角文字としてテキストオブジェクトが見つからなかった場合、全角文字扱いにして再検索させる。
            if s:iStart == s:iEnd
                let what = ToZenkaku(what)
            endif
        endif

        "非ASCII(全角)の場合
        if what !~ "[[:graph:]]"

            "開始位置を取得
            let pairs = s:getPairs(what)
            exec "normal! F". pairs[0]
            let s:aStart = getcurpos() | normal! l
            let s:iStart = getcurpos()

            "終了位置を取得
            exec "normal! f". pairs[1]
            let s:aEnd = getcurpos() | normal! h
            let s:iEnd = getcurpos()
        endif

        "範囲確認用
        "message clear
        "echom s:aStart[2]."-".s:iStart[2]."(".s:iStart[1].") , ".s:iEnd[2]."-".s:aEnd[2]."(".s:iEnd[1].")"

        return 0
    endf "}}}

    "変更後/追加する囲い文字の入力を受け付ける
    fu! s:getAfter() abort "{{{
        redraw! | echo s:cmd
        let wk = nr2char(getchar())

        if wk ==# "b"
            let wk = "("
        elseif wk ==# "B"
            let wk = "{"
        endif

        if wk == '<'
            "タグの場合は、開始タグ・終了タグ
            let l:tagStart =""
            while wk != '>'
                let l:tagStart .= wk
                redraw! | echo s:cmd.l:tagStart
                let wk = nr2char(getchar())
            endwhile
            let l:tagStart .= wk
            redraw! | echo s:cmd.l:tagStart

            let b:afterStart = l:tagStart
            let b:afterEnd   = substitute(l:tagStart,'<','</','')

        else
            "タグ以外の場合は、対になる記号、または、同一記号
            let pairs = s:getPairs(wk)
            let b:afterStart =pairs[0]
            let b:afterEnd   =pairs[1]
        endif

    endf "}}}

    "囲い文字の組み合わせを取得する
    fu! s:getPairs(surround) "{{{
        let mps_save = &mps
        "アングルブラケットをマッチペアに追加※「<」はタグ入力のトリガーなので、「>」用
        set mps+=<:>

        "全角カッコ系を追加
        set mps+=(:),「:」,{:},[:],【:】,『:』,<:>,≪:≫,《:》

        "マッチペアを検索
        let flgMps = 0
        for mp in split(&mps,',')
            let pairs = split(mp,':')
            if a:surround == pairs[0] || a:surround == pairs[1]
                let flgMps = 1 | break
            endif
        endfor

        "マッチペアを戻す
        let &mps = mps_save

        if flgMps == 0
            "マッチペアに該当しない場合は、同じ文字を設定する。
            let pairs[0] = a:surround
            let pairs[1] = a:surround
        endif

        return pairs

    endf "}}}

    "変更前のテキストオブジェクトの入力を受け付ける
    fu! s:getMotion(dotRep,...) abort "{{{
        let l:count = ""
        let l:where = ""
        let l:what  = ""

        "モーション文字列を取得
        if a:dotRep == v:true
            "ドットリピート時は前回値を使用
            let l:count = b:count
            let l:where = b:where
            let l:what  = b:what
        else
            "リピート時以外は、テキストオブジェクトの入力を受け付ける
            redraw! | echo s:cmd
            if a:0
                "可変引数あり(gzOperator()から呼び出し)時
                "すでに1文字目が入力されているとみなす
                let wk = a:1
            else
                "可変引数なし(ySurround()から呼び出し)
                let wk = nr2char(getchar())
            endif
            "無効な文字以外は終了
            if wk !~ '[[:graph:]]'
                redraw! | return 1
            endif

            while wk =~ "[0-9]"
                "数字が入力され続ける間、[count]に保持
                let l:count .= wk
                let s:cmd .= l:count | redraw! | echo s:cmd
                let wk = nr2char(getchar())
                if wk !~ '[[:graph:]]'
                    redraw! | return 1
                endif
            endwhile

            if wk =~ "^[ia]$"
                "テキストオブジェクトの場合
                let l:where = wk
                let s:cmd .= l:where | redraw! | echo s:cmd
                "もう1文字受け取ってテキストオブジェクトを完成させる
                let wk  = nr2char(getchar())
                if wk =~ "[0-9]"
                    "数字が入力された場合
                    if l:count == ""
                        "以前にcountが入力されていない場合、[count]として取得
                        while wk =~ "[0-9]"
                            let l:count .= wk
                            let s:cmd .= l:count | redraw! | echo s:cmd
                            let wk = nr2char(getchar())
                            if wk !~ '[[:graph:]]'
                                redraw! | return 1
                            endif
                        endwhile
                    else
                        "以前にcounttが入力されていた場合は、処理中断
                        redraw! | return 1
                    endif
                endif
                if wk !~ '[[:graph:]]'
                    redraw! | return 1
                endif
            endif
            "受け取った文字を連結(カーソル移動の場合も含む)
            let l:what = wk
            let s:cmd .= l:what | redraw! | echo s:cmd

            let b:count = l:count
            let b:where = l:where
            let b:what  = l:what

        endif

        "モーションの内側の範囲を取得
        if l:where == "" && l:what =="s"
            exec "normal v".l:count."il"
        else
            exec "normal v".l:count.l:where.l:what
        endif
        let s:iEnd = getcurpos()    | normal! o
        let s:iStart = getcurpos()  | normal! v

        return 0
    endf "}}}
"}}}

"ドットリピート関連{{{
    "リピート
    fu! s:dotRepeat() "{{{
        if exists("b:dotReady") && b:dotReady != 0
            if v:count!=0
                let b:count = v:count
            endif
            silent exec "call <SID>".b:dotRepeatFunc."(".v:true.")" | redraw!
        else
            normal! .
        endif
    endf "}}}

    "アンドゥ後のリピートに備える
    fu! s:undoRepeat(cmdUndo) "{{{
        if exists("b:dotReady") && b:dotReady != 0
            let b:dotReady = 2
        endif
        exec "normal! ".a:cmdUndo
    endf "}}}

    "リピート時の関数名を取得する
    fu! s:funcName(sfile) "{{{
        return substitute(a:sfile,".*_","","")
    endf "}}}
"}}}

"互換性オプションを復元
let &cpo = s:save_cpo

"test
"<a>'(hoge hoge )'</a>
"<a>'(hoge hoge )'</a>
"<a>'(hoge hoge )'</a>

今度こそ、大体やるべきことはやりつくした気がします。
そろそろ、%の実装に移ろうかと思います。

最後に

外部定義によって動作をカスタマイズする余地の欠片もない代物ですが、意外に簡単に実装できてしまいました。これは世の中にスクリプトが溢れかえるわけですね。しかし意図せず変な振舞も簡単に盛り込まれてしまうので、意外に気を抜けないとも感じました。

実は、まだvimファイルを開いて、:so %して動かす形でしかありませんが、pluginディレクトリに放り込めば、もうこのままでも動くのではないでしょうか。(あまり行儀は良くないですが)

でもまぁ、それも追々。テキストオブジェクトの2文字目の%にも対応できたときに、まとめて体裁を仕上げようかと思います。あちこちを気の向くままに弄っている最中のものをpluginディレクトリに置いたりすると、どのバージョンが今動いているのか訳が分からなくなるので、禁物なのです。(一度やった)

つまりちゃんと実用していないので、本当に実用に足るレベルで完成しているのかどうかはまだ謎なのです。
何せ「手習い」という記事なのでそれも止む無しなのですな。

テキストオブジェクト2文字目の%の作成については、次の記事に分けようと思います。(まだ手を付けていない)

また、ちゃんとプラグインっぽくするためには、<Plug>(PluginnameFuturename)というマッピングインターフェイスを介して呼び出すようにしておくと良いと言うことも学習しました。(:h <Plug>:h using-<Plug>参照)
そうすることで、カスタマイズがしやすくなるのですが、まぁ今のところは自分専用だし、必要ないかな。

そもそも、当記事にて作成した代物は、echoで表示するための文字列を、関数内にベタ書きで持つという暴挙に出ているため、そんなカッコいい姿になる事はなさそうです。(そもそもが既存プラグインの猿真似なので、適切なプラグイン名がつけようがないというのもある。)

マッピングインターフェイスを挟むことで vimrc 側でカスタマイズ可能になり、%による対になる括弧などの組み合わせの内部(i%)、外部(a%)、囲み(s%)のテキストオブジェクトにも対応したバージョンが完成しました。try文を使ったエラー制御も使ってみたり、いろいろと楽しかったです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?