概要
Vim のオペレータを自作してみる事に関する、ごく初歩的な試みです。
とは言え、特にオペレータを作ってやりたいことがある、と言うわけではありません。
ただ、ちょっとした手順を何度か繰り返したい時に、qのレコーディングであれこれするよりも、気軽に中身を書き換えて使いまわせる自前のオペレータのスケルトン的な物があると便利かもなー、と思って書き始めたものです。
実際のところは、Vim ヘルプを見ていて、やってみたくなったというのが一番の理由です。
世の中にはいろいろと便利なオペレータを追加するプラグインもありますし、そもそも自作オペレータの追加を容易にするプラグインというのもあるらしいので、普通はそういうものを使うのだと思います。
・・・が、あえて自分で作ってみるという事に意義があると思うのです。
車輪の再発明ではありません。車輪がすでに発明はされていることは知っています。
既製品の車輪が手に入らない環境に住まうことを強いられている(社内ルールと言います)ため、拙い車輪でも手作りしてみるというわけです。
基本的には舶来品は全てご禁制(ネットという大海を経由するだけでアウトなので、国産どころか自家製だろうが当然 NG)なのです。
お上(情報システム部)公認のもの以外は存在を許されません。そもそもエディタ選択の自由もない。もはや宗教弾圧である。・・・だのに役員共は生産性を上げろという。あと 7zip 禁止して laplus 強要ってどういうつもりなのか?・・・いや愚痴はやめよう。
Vim が Linux に最初から入っててくれて本当に良かった。その理由を挙げなかったら、GVim のダウンロード申請も通らなかっただろうし。っていうかあの人たち vi/Vim の名前すら知らなかったし。本当に勘弁して頂きたかった。
・・・というわけで、その場で書くだけで簡単に機能追加・変更ができる使えるスクリプトがあると、本当に本当の本当にとっても助かるのです。
愚痴じゃないですよ?幸運に感謝してるんですよ。いや本当に。
環境
GVIM 8.2.1287 (kaoriya 2020/07/24)
オペレータって?
オペレータとは、通常モードから、オペレータ待機モードに入る時に押されるコマンドです。
c
とかd
とかy
とか・・・いろいろあります。
詳しくは:h operator
参照なのです。
オペレータが受け取るもの(モーション)
オペレータ待機モードでは、続けてモーションを受け取ります。モーションというのはカーソル移動や、テキストオブジェクトの事です。
これまた:h motion.txt
や:h movement
を参照するのが一番です。
カーソル移動
オペレータに続けてカーソル移動コマンドを行なうと、そのコマンドでカーソルが動く前(A 地点)と、動いた後(B 地点)の、 A 地点~ B 地点の間の文字列を対象に、オペレータが何かを行います。
- オペレータ
d
に対して、dw
なら、デリート、ワード。単語削除。 - オペレータ
c
に対して、c2w
で、チェンジ、ツー、ワード。2単語置き換え。
・・・みたいな。
とはいえ、yj
のように行を移動するカーソル移動を行うと、行単位が対象になります。
また、該当行のみを対象にしたい場合は、オペレータを続けて打つという vi 互換のコマンドがありますね。cc
とかdd
とかyy
とか。
:h left-right-motions
や:h word-motions
や、:h object-motion
で動ける範囲だと、文字列を対象。
:h up-down-motions
だと、行単位が対象になるような気がします。
豆知識ですが、dj
は行単位になりますが、dvj
とすると文字単位に削除することができます。たまに便利です。
逆に、dw
は単語単位(文字単位)ですが、dVw
とすることで、行単位として振舞わせることができます。dVw
の場合は、dd
した方が早いですが、どれかのオペレータで何か役に立つかもしれないので、覚えておきましょう。
#むしろ、自前でテキストオブジェクトの入力を受け付ける時に気を付けるべき点ではないかと思いました。
・・・と言いつつ、次の記事ではこの移動単位の強制指定(forced-motion)に対応していないわけですが
同様に、d<C-V>9j
とやると、<C-V>9jd
と同じことが起こります。なかなか面白いです。
知らない人の前でやると驚かれるかもしれませんが、vimmerが周りに一人もいない私には関係のないことです。
ちなみにd<C-V>9j
と<C-V>9jd
は、単体で見ると同じ編集が行われますが、.
でリピートと組み合わせたときの威力は前者の方が強力です。上記操作を行った後に例えば2.
、1.
、.
としてみてください。
・・・どうです、わかりましたか?効率上がりそうでしょう?
もし知らなかったvimmerなら、ニヤリと笑えるのではないでしょうか。私は笑いました。
当然ですが、物理行単位にしか働かないので、折り返していると混乱することがあるので、過信は禁物です。
論理行として働いてほしくてd<C-V>9gj
等としてもいまいち結果が思わしくありません。
折り返されている行の上で色々試してみたところ、countをつけずにd<C-V>gj
としてもdvgj
と同じ結果になったので、欲張りすぎは良くないようです。
詳しくは:h forced-motion
を参照なのです。
テキストオブジェクト
オペレータ待機モード中に、テキストオブジェクトを表わすコマンドを入力することで、テキストオブジェクトが示す範囲を対象に、オペレータが処理を行います。
ちなみにテキストオブジェクトは、オペレータ待機モードだけではなく、ビジュアルモードでもその威力を発揮します。
詳しくは:h objects
や:h text-objects
を参照なのです。
………しかし、書けば書くほど、下手なヘルプの引用にしかならないという自体に直面し、ただただ Vim ヘルプの素晴らしさに圧倒されるばかりですね。
へこたれずに、多少は自分なりに書き続けてみます。
とは言え、『テキストオブジェクトにはこれこれこういうものがあります』と言う様な事を、記事内でわざわざ一覧にしたりはしません。それこそヘルプ嫁というやつです。
テキストオブジェクトは2文字で表されます。
うまく言えませんが、1文字目が範囲、2文字目が対象、・・・という感じです。
「ドコ」の「ナニ」という順番です。「中、家の」「周り、机の」みたいな。
1文字目の範囲について、自分は、i
(inner :内側)、a
(around:周り)と覚えています。
ヘルプを読んでみると、どうやらa
は"a"(不定冠詞) らしいのですが? 耳馴染みがないので、アラウンドと独習してしまっていました。
・・・意味的には別に間違ってはいないはず。(むしろ「周り」の方が意味が通じるような気がする)
2文字目の対象については、w
が単語だったりするのでカーソル移動と似てるのかな、と最初は思ってしまいがちですが(そんなことない?)、実際には全然違います。
b
は後方の単語などではなく、ブロックを意味しますし、l
は1文字右などではなく、ライン(1行)です。
h
、j
、k
等は意味を持ちません。
実際に使う時は、例えば、ciw
で「単語の内側を置換(消して、入力モードに遷移)」という意味になります。
オペレータc
に対して、テキストオブジェクトを用いてciw
とする場合と、モーションを用いて同じことをしたい場合には、b
を前置してbcw
とすれば同じ結果を得ることができます。
ただし、ciw
とbcw
の二者では、.
で繰り返したときに違いが出てきます。
ciw
はこの3文字のキーストローク自身が一つのオペレータの操作です。つまり.
で繰り返される単位になるのです。
bcw
は、実際にはb
(カーソル移動)とcw
(置換)の2つの操作に分割されています。なので、次の単語中にカーソルがある状態で.
を押下しても、再度実行されるのはcw
だけであり、期待した結果が得られない可能性があります。
具体的に言うと単語の先頭にカーソルがあるときにしかciw
と同じ結果は得られません。
b.
とする必要があると言ったほうが分かりやすいでしょうか。
若干話が脱線しますが、もう少し実例寄りだと、ドキュメントにテーブル名やカラム名が全部大文字で書かれていて、それを切り貼りしながら SQL 文を作成していたら、むしろ予約語が小文字で、非予約語が大文字という有様になる事とかあるじゃないですか。ありませんか?
普段は別に気にせずそのまま使いますが、このクエリはチョイチョイ使いそうなので残しておきたいな、とか思う時には、ちょっと見た目にも気を遣うじゃないですか。
そういう時に、g~iw
とかやっておいて、w
やb
で移動しながら.
すると、SQL 文の大文字小文字を単語単位で整えるのが楽ちんです。
j
やk
で行を移動したときに、カーソル位置が単語の途中に乗ってしまった時にも、気にせず.
することができます。
閑話休題。
まぁ、もっと簡単にテキストオブジェクトの優れている点を挙げろ、と言われればば、i"
、i'
、i<
等といった、括り文字の内側、という反則的な便利さ等があるでしょう。
上記の例などから、テキストオブジェクトの方がカーソル移動よりも優れている、と一概に言えるかというと、単純にそういうワケでもありません。
オペレータによっては、モーションでカーソル位置が移動する、という作用まで織り込んで.
で繰り返すことができるためです。
また、カーソルの進む方向にだけオペレータの作用を留めたい場合は、あえてカーソル移動を選ぶことがあります。
gqj
などが好例です。・・・というかそれくらいしか思いつかないし、gqap
という使い方もあるので、カーソル移動だからとかテキストオブジェクトだからどうだ、と言う話とは関係ないのですが。
だって、自前で字下げや改行の微修正とかしてる場合に、段落単位のgqap
だと、整形し直して欲しくないところまで整形してくれちゃうからヤなんですよ。あとgqj...
の方がタイプが楽だし。
再び話が脱線していきますけど、プロポーショナルフォント表示が初期設定になってるメーラーが多くて、等幅フォント的に整形したつもりではガッタガタの意味の分からない改行が散りばめられた怪文になるので、何も考えずgq
任せに整形するというわけにはいかないんですよね。
再び閑話休題。
当然、まだ私が知らないだけで、gq
以外にも、カーソル移動の方が捗るぜ!というオペレータがあるかもしれません。
モーションを受け取った後に、オペレータが行うこと
それはもう、オペレータによって千差万別、というか、オペレータの数だけ、行われることはあります。
もちろん実際には千も万もありません。並みのエディタよりは多い事は確かですが・・・いやむしろ「テキストに対する作用」の種類としては少ないのかも?
オペレータとモーションの掛け合わせによって、結果として起すことができる事象のバリエーションが凄まじい事になっている。というのが正しいですね。
カーソル移動の [COUNT] まで込みで考えたら、余裕で万越えるどころか、もはや無限といってもいいのかも。素晴らしい・・・。
・・・ともあれ、多様なオペレータを含む数多くのコマンドが用意されているので、vi のノーマルモードってほとんど全ての1文字アルファベットに、大文字と小文字を区別して、何らかの有意なコマンドが割り振り済みなんですよね。
というか、単打で打てるオペレータは最初に紹介したc
、d
、y
の3つしかありません。ほかは2ストロークだったり、修飾キー込みだったりします。suround.vim が囲うためのオペレータとしてy
を使っているのは苦肉の策のように感じます。ですが、i
やa
のバリエーションとしてのs
という範囲指定は文法上わかりやすく感じます。手っ取り早くオペレータ待機モードへ入るために、y
を使うという思想には共感できます。(使ってませんけどね。なぜなら職場にはネット環境がありませんからね。git何それおいしいの?windows上でbashが使えるのはすごく魅力的だけど、busyboxで概ね大満足)
そういう状況なので、「自分のやってほしいコト」一つずつのために、空いているアルファベットを捻り出すことは容易ではありません。むしろデフォルトの機能を殺すことを覚悟で載せていく必要があります。
「コマンドなんちゃらと同じ。推奨されない。」とか書かれているようなコマンドなら遠慮なく潰せますが、素の状態の Vim をこよなく愛してしまっている身としては、「ピュアな Vim をあまり穢したくない」という訳の分からないジレンマを抱えることになります。(kaoriya 版という時点で既にピュアではないのですが、それは言わないお約束)
後は、<Leader>
や<LocalLeader>
に押し込むとか。修飾キーと同時押しにするとか。n打鍵にするとか。
他には、これはもう本当に最終手段ですが、ファンクションキーに乗せるか・・・、という苦渋の決断を迫られます。
また、他のプラグインを入れたりしている場合は、それらとの兼ね合いを考える必要もありそうですね。(私はプラグインを殆ど入れていないので関係ないのですが)
まぁ、そんなわけで、任意の1文字を潰して、その1文字のオペレータでやる内容は、その都度都度、自分なりに書き換えてしまおう。・・・というのが今回の記事の(やっと)目的でした。
というわけで、とりあえずZ
を潰して、なにやらやってみる、という物を、ペペッと書いてみました。
オペレータ「Z」実装サンプル
通常モードでZ{motion}
または、ヴィジュアルモードでZ
とする事で、モーションまたは選択の範囲が何行目~何行目で、何という文字列が対象だったのかを echo 表示するだけのオペレータです。
実体はechorange()
という関数です。
・・・どうしよう。
ほぼほぼ:h map-operator
のサンプルの丸パクリだから、解説するほどのことがまるでない。
いやマジでヘルプをご覧あれ。
動作を試してみる場合は、適当な名前に拡張子.vim
をつけて保存して、Vim で開いてから:so %
として自身を読み込ませてから使ってみてください。
ヴィジュアルモードは、文字選択、行選択、矩形選択に対応しています。
オペレータ自身への [count] には対応できていません。モーションへの [count] はうまくいくんですが。
オペレータとして今後の課題ですね。
nnoremap <silent> Z :set opfunc=<SID>echorange<CR>g@
vnoremap <silent> Z :<C-U>call <SID>echorange(visualmode(),1)<CR>
fu! s:echorange(type,...)
"状態退避&変更
let sel_save = &selection
let &selection = "inclusive"
let reg_save = @@
"可変長引数あり(vmapから)
if a:0
"範囲再選択
silent exe "normal! gv"
"開始・終了行を取得
let line1 = line('.') | normal! o
let line2 = line('.') | normal! o
let first = line1<line2? line1:line2
let last = line1<line2? line2:line1
"選択範囲をヤンク
silent exe "normal! gvy"
"上記以外、g@(nmapから)
else
"モーションの開始・終了マークから、開始・終了行を取得
let first =line("'[")
let last =line("']")
"モーション範囲をヤンク
if a:type == 'line'
silent exe "normal! '[V']y"
else
silent exe "normal! `[v`]y"
endif
endif
"タイプ、および、開始・終了行を表示
echon a:type
echon ': ' . first . ' - ' . last
echon "\n"
"レジスタ内容を表示
if a:type == 'char' && a:type != 0
echon ' "' . @@ . '"'
else
echon @@
endif
"状態復帰
let &selection = sel_save
let @@ = reg_save
endfu
コマンドでも同じことをやってみたい(オマケ)
せっかく作った処理なので、オペレータからだけではなく、コロンコマンドでも実行してみたいな、と思ったので、少し追記してみました。
コメントの「(★)」箇所が追加した部分です。
ユーザ定義コマンドの場合は、-count
を付けると [count] 指定できるようになるらしいのですが、-range
指定と同時に指定する場合はどのようにしたらいいのかまだわかっていません。
ザコが手探りしてる感満載の記事でしょう?
"コマンド定義(★)
com! -buffer -range Echorange call <SID>echorange('command',<line1>,<line2>)
nnoremap <silent> Z :set opfunc=<SID>echorange<CR>g@
vnoremap <silent> Z :<C-U>call <SID>echorange(visualmode(),1)<CR>
fu! s:echorange(type,...)
"状態退避&変更
let sel_save = &selection
let &selection = "inclusive"
let reg_save = @@
"コマンドから (★)
if a:type == 'command'
"開始・終了行を取得
let first = a:1
let last = a:2
"行範囲をヤンク
silent exe first . ',' . last .'y'
"コマンド以外で、可変長引数あり(vmapから)
elseif a:0
"範囲再選択
silent exe "normal! gv"
"開始・終了行を取得
let line1 = line('.') | normal! o
let line2 = line('.') | normal! o
let first = line1<line2? line1:line2
let last = line1<line2? line2:line1
"選択範囲をヤンク
silent exe "normal! gvy"
"上記以外、g@(nmapから)
else
"モーションの開始・終了マークから、開始・終了行を取得
let first =line("'[")
let last =line("']")
"モーション範囲をヤンク
if a:type == 'line'
silent exe "normal! '[V']y"
else
silent exe "normal! `[v`]y"
endif
endif
"タイプ、および、開始・終了行を表示
echon a:type
echon ': ' . first . ' - ' . last
echon "\n"
"レジスタ内容を表示
if a:type == 'char' && a:type != 0
echon ' "' . @@ . '"'
else
echon @@
endif
"状態復帰
let &selection = sel_save
let @@ = reg_save
endfu
応用例
その1
上記の例では、さすがに何の役にも立ちませんので、もう少し意味ありげな使い方を模索してみました。
・・・といっても、範囲をソートするだけで、:'<,'>!sort
と変わらないため、相変わらず実用性は皆無です。というか内部的にもモロに:'<,'>!sort
しているだけです。
一応、テンプレとして echorange() は残しつつ、sortrange() を追加して、共通部分を切り出しました。
今更ですが、可変引数については、:h a:0
または、を見るとわかります。
:h ...
でも同じですし、a:000
でもヘルプ同じです。
後は、Leaderキーを使ったマップにしてあります。
:h <leader>
、:h <Leader>
、:h mapleader
を参照の事。
"範囲を表示するだけ
com! -buffer -range Echorange call <SID>echorange('command',<line1>,<line2>)
nnoremap <silent> <LocalLeader>z :set opfunc=<SID>echorange<CR>g@
vnoremap <silent> <LocalLeader>z :<C-U>call <SID>echorange(visualmode(),1)<CR>
fu! s:echorange(type,...) "{{{
call s:fuStart(a:type,a:000)
"タイプ、および、開始・終了行を表示
echon a:type
echon ': ' . s:first . ' - ' . s:last
echon "\n"
"レジスタ内容を表示
if a:type == 'char' && a:type != 0
echon ' "' . @@ . '"'
else
echon @@
endif
call s:fuEnd()
endfu "}}}
"範囲をソートする
com! -buffer -range Sortrange call <SID>sortrange('command',<line1>,<line2>)
nnoremap <silent> <LocalLeader>s :set opfunc=<SID>sortrange<CR>g@
vnoremap <silent> <LocalLeader>s :<C-U>call <SID>sortrange(visualmode(),1)<CR>
fu! s:sortrange(type,...) "{{{
call s:fuStart(a:type,a:000)
"行単位以外は何もしない
if a:type != 'line' && a:type != 'V' && a:type!='command'
return
endif
exe s:first . ',' . s:last .'!sort'
"3
"2
"1
call s:fuEnd()
endfu "}}}
"--------------------------------------------------------------------------------
"以降は共通処理{{{
"--------------------------------------------------------------------------------
fu s:fuStart(type,pList) "{{{
set lazyredraw
"状態退避&変更
let s:save_wi=winsaveview()
let s:sel_save = &selection
let &selection = "inclusive"
let s:reg_save = @@
"コマンドから
if a:type == 'command'
"開始・終了行を取得
let s:first = a:pList[0]
let s:last = a:pList[1]
"行範囲をヤンク
silent exe s:first . ',' . s:last .'y'
"コマンド以外で、可変長引数あり(vmapから)
elseif a:type == 'v' || a:type == 'V' || a:type == "<c-v>"
"範囲再選択
silent exe "normal! gv"
"開始・終了行を取得
let line1 = line('.') | normal! o
let line2 = line('.') | normal! o
let s:first = line1<line2? line1:line2
let s:last = line1<line2? line2:line1
"選択範囲をヤンク
silent exe "normal! gvy"
"上記以外、g@(nmapから)
else
"モーションの開始・終了マークから、開始・終了行を取得
let s:first =line("'[")
let s:last =line("']")
"モーション範囲をヤンク
if a:type == 'line'
silent exe "normal! '[V']y"
else
silent exe "normal! `[v`]y"
endif
endif
endf "}}}
fu s:fuEnd() "{{{
"状態復帰
let &selection = s:sel_save
let @@ = s:reg_save
call winrestview(s:save_wi)
set nolazyredraw
endf "}}}
"}}}
その2
2020/11/08追記。
上記の例では、行の範囲に対して、Exコマンドで編集しているだけだったので、モーションの範囲に対して、何らかの編集処理をして、直接置き換える、というものを目指してみました。
行範囲に対して処理を行う場合は、Exコマンドで直に開始行,終了行s/hoge/fuga/
とかやった方が楽に編集できますが、1行内の限られた範囲に対して正規表現を使って編集した場合はそうはいきませんものね。
ということで<Leader>w
とすると、全角文字と半角文字の間に、半角空白を埋め込むという処理を書いてみました。必ずしも自分がそうしているというわけではないのですが、オペレータとしてやってみたいという処理が、特に思いつかなかったので、とりあえず・・・。
モーションとして$
で行末まで範囲指定した場合と、ちょうど行末直前までを範囲指定したときで、ちょっとだけ気を付ける必要があったのが、実装してみて驚きでした。
相変わらず、行範囲だった場合には、Exコマンド頼みなので、新設したopFuncPut()関数が、行単位の時に正しく動くかどうかについては、実はまだ試していません。
" vi: set et ts=4 sts=4 sw=4 fdm=marker :
scriptencoding utf-8
"半角と全角の間に半角スペースを入れる (大変これを嫌う人もいますが、 W,B が利くようになるのです。。)
com! -buffer -range Sortrange call <SID>spaceWordSep('command',<line1>,<line2>)
nnoremap <silent> <LocalLeader>w :set opfunc=<SID>spaceWordSep<CR>g@
vnoremap <silent> <LocalLeader>w :<C-U>call <SID>spaceWordSep(visualmode(),1)<CR>
fu! s:spaceWordSep(type,...) "{{{
call s:opFuncStart(a:type,a:000)
messages clear
if s:isLine(a:type) == v:true
silent exec s:first.','.s:last.'s/\m\([[:alnum:][:punct:]]\)\([^[:alnum:][:punct:][:blank:]]\)/\1 \2/ge'
silent exec s:first.','.s:last.'s/\m\([^[:alnum:][:punct:][:blank:]]\)\([[:alnum:][:punct:]]\)/\1 \2/ge'
else
let @@ = substitute(@@, '\m\([[:alnum:][:punct:]]\)\([^[:alnum:][:punct:][:blank:]]\)' , '\1 \2' , 'g')
let @@ = substitute(@@, '\m\([^[:alnum:][:punct:][:blank:]]\)\([[:alnum:][:punct:]]\)' , '\1 \2' , 'g')
call s:opFuncPut(a:type)
endif
"abcあいう1,2
call s:opFuncEnd()
endf "}}}
"--------------------------------------------------------------------------------
"以降は共通処理{{{
"--------------------------------------------------------------------------------
fu! s:opFuncStart(type,pList) "{{{
set lazyredraw
"状態退避&変更
let s:wi_save=winsaveview()
let s:sel_save = &selection
let &selection = "inclusive"
let s:reg_save = @@
"コマンドから
if a:type == 'command'
"開始・終了行を取得
let s:first = a:pList[0]
let s:last = a:pList[1]
"行範囲をヤンク
silent exe s:first . ',' . s:last .'y'
"コマンド以外で、可変長引数あり(vmapから)
elseif a:type ==# 'v' || a:type ==# 'V' || a:type == "<c-v>"
"範囲再選択
silent exe "normal! gv"
"開始・終了行を取得
let line1 = line('.') | normal! o
let line2 = line('.') | normal! o
let s:first = line1<line2? line1:line2
let s:last = line1<line2? line2:line1
"選択範囲をヤンク
silent exe "normal! gvy"
"上記以外、g@(nmapから)
else
"モーションの開始・終了マークから、開始・終了行を取得
let s:first =line("'[")
let s:last =line("']")
"モーション範囲をヤンク
if a:type == 'line'
silent exe "normal! '[V']y"
else
silent exe "normal! `[v`]y"
endif
endif
endf "}}}
fu! s:opFuncPut(type) "{{{
"行単位
if s:isLine(a:type) == v:true
"echom 'line'
"echo s:first.",".s:last."d _"
silent exec s:first.",".s:last."d _"
silent normal P
else
"echom 'not line'
silent exe 'normal! `[v`]"_d'
let s:wi_work = winsaveview()
echom "old_col:" . s:wi_save["col"] . " now_col:" . s:wi_work["col"]
if s:wi_save["col"] == s:wi_work["col"]
silent normal P
else
silent normal p
endif
endif
endfunc "}}}
fu! s:opFuncEnd() "{{{
"状態復帰
let &selection = s:sel_save
let @@ = s:reg_save
call winrestview(s:wi_save)
set nolazyredraw
endf "}}}
fu! s:isLine(type) "{{{
if a:type == 'line' || a:type ==# 'V' || a:type =='command'
return v:true
else
return v:false
endif
endfunc "}}}
"}}}
最後に
任意の箇所で、任意のテキスト範囲に対して、正規表現使いたい放題で任意の処理を、任意のタイミングで発動できる、ってすごいことですよね。
任意のテキスト範囲(テキストオブジェクトですな)を追加するプラグインもあるので、そのうち手を出してみたいですねぇ。
:h 'operatorfunc'
のような仕掛けが用意されているオペレータとは違い、テキストオブジェクトを一から自前で作ることは、なかなか難しいらしく、もし完全自前で頑張ろうと思ったら、せいぜいオペレータ待機モードでのマッピングをいろいろと頑張るしかなさそうなのでしょうか。
ちょっと調べた感じだと、i
やa
に続けて使うもの(%
を増やしてみるのが便利そうです。)の場合は、オペレータ待機モードでのマッピングに書けそうですが、i
やa
そのものに該当するようなもの(surrround.vimのs
等)の場合は、ノーマルモードのマッピングに書いていく必要がありそうです。
surround.vim相当の機能をボチボチと独自実装してみたので、おいおい別記事にまとめます。
→書きました。vimテキストオブジェクト(風に振舞うキーマップ)の自作(手習い)
ガチガチに書くことはできましたが、外部定義でカスタマイズ可能なようにプラグインを書く人はすごいですね。