概要
前回のオペレータの自作から引き続き、今度はテキストオブジェクトを自作してみようと思います。
目指すは、以下の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
とかしてみてください。
" 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文字目のc
やd
やy
が発動するようなomap
に乗せる関数では、上記のような仕掛けのために用いることは難しそうです。
そもそも、ys
はyankの働きをしないので、y
オペレータとして待機している時点で待ったをかける必要があります。
ちなみに、単打で打てるオペレータがc
、d
、y
なので、囲い文字を変えたり消したりはともかく、囲い文字だけYankしたいってこたぁねぇだろ、ということで、ys
が囲み追加オペレータになっているものと思われます。
sy
等にすると、s
出来なくなってしまうのでしょうか?私には恐ろしくてできません。。
とりあえず、surround.vim の真似をしてみる。
という事で、omap
ではなくmap
に、cs
やds
やys
を書いてしまおうという方針転換をしました。
※本来はnmap
でやるべきなのでしょうが、この時点の筆者はまだ気が付いていませんでした。。
この時点から、オペレータ待機モードでのリマップ(omap
)はもう出てきません。
ファイル名に偽りありです。
ひとまずガーッと書いてみました。
- ddが遅延するのが気持ち悪い! (
noremap
のせいです) - ドットリピートができない! (g@は何も考えなくてもドットリピートできたのに・・・)
- ysの動きが変! (この時はまだ気がついてもなかった)
という問題点を抱えたバージョンです。
生まれたての小鹿のようなものです。微笑ましいですね。
" 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
が打たれた場合は、viw
とvaw
を行い、それぞれの選択開始・終了位置を保持します。
次にgetAfter()
関数です。
こちらは、単一括り文字以外で、組になる括り文字の相方を探すための処理になります。
括弧などの組み合わせについては、:h 'matchpairs'
を参照です。
vimrcに全角括弧系をvimrcに追加していたので、それも含めて処理してくれるようにしてあります。
ここまでできれば、もう完成したも同然でした。
まだこの時点では、ys
の動きは本家と異なっています。
本家だとysiw'
とするべきところが、ysw'
となります。
ysaw'
相当の振る舞いはまだ実現できていませんでした。
そんなことよりも、こんなに簡単にできちゃった!という喜びが、ドットリピートができない!と言うことに気づいた瞬間、絶望に塗り替えられました。
2度目の挫折
ドットリピートができない。ハッキリ言ってそんなコマンドにはあんまり意味がない。
もともとのsurround.vimはいったいどうだったんだ!・・・と調べてみました。
どうやら単体では対応しておらず、別途プラグインを導入することで実現しているようでした。
何ということでしょう。surround.vimは中身を読んでなかったから、そんなことにも気が付きいていませんでした。
当然対応しているものとばかり・・・。
早速、repeat.vimの真似をしてみる。
流石に独自実装を貫こうなどという気概はどこかに吹き飛びました。(元からないけど)
というわけで、repeat.vimの中身を読んでみました。
.
をリマップして、関数を呼び出しています。・・・なるほどなるほどー。
無我夢中にドットリピートに対応させました。
ついでにnoremap
からnnoremap
に変更しました。これでdd
等が遅延して気持ち悪い問題も解決しました。
ys
の挙動が変なのは、この時点ではまだ気付いてもいないので相変わらずです。
" 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()
関数がキモになります。
-
cs
やds
やys
が押されたときに、フラグを立てておいて、テキストが変更されたときそのフラグを下げる。 -
.
押下時にそのフラグが立っている時は、さっき実行したのと同じ関数を実行する。
という感じで実装しました。
1を立てておくと、自分自身が編集した事で発生するイベントで0になってしまうので、2を設定してあります。
なんだかかっこ悪いです。
3度目の挫折
実はまだ問題が残っており、アンドゥを挟むと、次のドットリピートが効かなくなります。
cs'"
してから、.
で繰り返していって、間違えたと思ったらu
して、再度.
すると、またcs'"
が続いていってほしいじゃないですか。
続かないんですよ、これが。ファッキンこの役立たずめ!
再び、repeat.vimを読んでみる。
もう一度ザッと目を通したところ、u
やU
もリマップされていました。
これでアンドゥ後のドットリピートも問題なく動くようにしているのではないかと推測されます。
マップされている関数から実装を追っかけてみようとしましたが、なんだかややこしかったので、自分なりに簡単に実現する方法を取りました。結果的にはたぶん似たような対応になったのではないかと思います?
このバージョンでは以下の対応をしました。
- アンドゥされた後にもドットリピートが続くようにしました。
-
ys
が受け取るものを、サラウンドオブジェクトではなく、テキストオブジェクトに変えました。 - showcmdオプションでオペレータの入力途中までのストロークを表示してくれるのですが、自前の処理に入ると何も出なくなってしまうので、echoで表示するように対応。
何のことを言っているのかは:h 'showcmd'
参照の事。表示箇所が変わってしまうのがちょっと不満です。
" 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。
ついでに、行数の削減を少々行いました。
" 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
に修正しました。 - マーカー折り畳みにより、スクリプトソースをブロック分けしました。
- オペレータ
gus
、gUs
、g~s
を追加しました。
囲い文字の小文字と大文字を置き換えます。タグにしか意味がありません。
タグを大文字にすることもあまりないと思いますが、とりあえずセットで作っておきました。 - オペレータ
gzs
、gZs
を追加しました。
hz_ja.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()
関数に名前を変えました。
そして、「gzs
、gZs
」だけではなく、「gzi
gZi
gza
gZa
」を追加しました。
" 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>
改善後に気が付く問題点
「サラウンドオブジェクトだけじゃなくて、テキストオブジェクトも対象にする」という事しか考えていなかった私は気が付いていませんでした。
このgz
とgZ
を、オペレータとしてみた場合、カーソル移動が利かないのです。
こんなものは、もはやとんだ欠陥オペレータと言っていいでしょう。
一人で作っていても、これほど不具合てんこ盛りやなので、プラグインとして公開し多数の目にさらされ、メンテナンスの責務を負うということは非常に恐ろしいことであるな、と改めて思いました。
稚拙な学習過程として、こうしてダラダラとした記事に書いてあげるくらいの恥をかいている方が気楽でよいです。
一通り機能がそろったかも版
「gzs
、gZs
、gzi
、gZi
、gza
、gZa
」と6つも定義されていたマッピングを「gz
とgZ
」の二つにまとめました。
とってもスッキリしましたね。
後は、自前で入力を受け持っていた箇所で、ESCを押して中断したいという場合に、一発で抜けられるようにしたつもりです。
getMotion()
関数は、自前でテキストオブジェクトを受け付けるための関数でしたが、もともとySurround()
関数の中から呼び出していました。囲われる範囲の2iw
とかap
とかを受け付ける関数です。ちなみに現状ではi2w
とかできないのが課題です。修正の目途はたっていますが、まだ未着手です。忘れそうなのでここにもTODOとして書いておきましょう。⇒omap_test9.vimで対応しました。
今回は、getMotion()
関数をgzOperator()
関数からも呼び出すように修正を加えました。
gzOperator()
では、s
を受け取った場合のサラウンドオブジェクトに対する処理と、それ以外を受け取った場合にはテキストオブジェクトまたはカーソル移動として処理させる必要があります。
ySurround()
では、もうs
まで入力されてしまっていて、次にi
かa
が入力される想定なのですが、gzOperator()
では、i
かa
(やそれ以外のカーソル移動)が入力された後に、テキストオブジェクトの残りを受け取る必要があります。
なので、そのまま呼び出すわけにはいきませんでした。若干ソースが汚くなった気がします。即興スクリプトらしいっちゃらしいですが。
" 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"
実行時の選択範囲と同じ相対範囲を対象に"
で括る、という動きをします。まぁ、何か使い道があると思いましょう。
" 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
は、カラム位置についても、相対的に左右方向へ終了位置をズラす様にしてあるので、なんだか小汚い実装になっています。
" 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文を使ったエラー制御も使ってみたり、いろいろと楽しかったです。