概要
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
だと、行単位が対象になるような気がします。
テキストオブジェクト
オペレータ待機モード中に、テキストオブジェクトを表わすコマンドを入力することで、テキストオブジェクトが示す範囲を対象に、オペレータが処理を行います。
ちなみにテキストオブジェクトは、オペレータ待機モードだけではなく、ビジュアルモードでもその威力を発揮します。
詳しくは: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文字アルファベットに、大文字と小文字を区別して、何らかの有意なコマンドが割り振り済みなんですよね。
なので、「自分のやってほしいコト」一つずつのために、空いているアルファベットを捻り出すことは容易ではありません。むしろデフォルトの機能を殺すことを覚悟で載せていく必要があります。
「コマンドなんちゃらと同じ。推奨されない。」とか書かれているようなコマンドなら遠慮なく潰せますが、素の状態の Vim をこよなく愛してしまっている身としては、「ピュアな Vim をあまり穢したくない」という訳の分からないジレンマを抱えることになります。(kaoriya 版という時点で既にピュアではないのですが、それは言わないお約束)
後は、<Leader>
や<LocalLeader>
に押し込むとか。修飾キーと同時押しにするとか。n打鍵にするとか。
他には、これはもう本当に最終手段ですが、ファンクションキーに乗せるか・・・、という苦渋の決断を迫られます。
また、他のプラグインを入れたりしている場合は、それらとの兼ね合いを考える必要もありそうですね。(私はプラグインを殆ど入れていないので関係ないのですが)
まぁ、そんなわけで、任意の1文字を潰して、その1文字のオペレータでやる内容は、その都度都度、自分なりに書き換えてしまおう。・・・というのが今回の記事の(やっと)目的でした。
というわけで、とりあえずZ
を潰して、なにやらやってみる、という物を、ペペッと書いてみました。
オペレータ「Z」実装サンプル
通常モードでZ<motion>
または、ヴィジュアルモードでZ
とする事で、モーションまたは選択の範囲が何行目~何行目で、何という文字列が対象だったのかを echo 表示するだけのオペレータです。
実体はechorange()
という関数です。
・・・どうしよう。
ほぼほぼ:h map-operator
のサンプルの丸パクリだから、解説するほどのことがまるでない。
いやマジでヘルプをご覧あれ。
動作を試してみる場合は、適当な名前に拡張子.vim
をつけて保存して、Vim で開いてから:so %
として自身を読み込ませてから使ってみてください。
ヴィジュアルモードは、文字選択、行選択、矩形選択に対応しています。
オペレータ自身への [count] には対応できていません。モーションへの [count] はうまくいくんですが。
オペレータとして今後の課題ですね。
opfunc_test.vim
nnoremap <silent> Z :setopfunc=<SID>echorange<CR>g@
vnoremap <silent> Z :<C-U>call<SID>echorange(visualmode(),1)<CR>fu!s:echorange(type,...)"状態退避&変更let sel_save =&selectionlet&selection="inclusive"let reg_save = @@
"可変長引数あり(vmapから)ifa:0"範囲再選択silent exe "normal! gv""開始・終了行を取得let line1 =line('.')| normal!olet line2 =line('.')| normal!oletfirst= line1<line2? line1:line2
letlast= line1<line2? line2:line1
"選択範囲をヤンクsilent exe "normal! gvy""上記以外、g@(nmapから)else"モーションの開始・終了マークから、開始・終了行を取得letfirst=line("'[")letlast=line("']")"モーション範囲をヤンクifa:type=='line'silent exe "normal! '[V']y"elsesilent exe "normal! `[v`]y"endifendif"タイプ、および、開始・終了行を表示echona:typeechon': '.first.' - '.lastechon"\n""レジスタ内容を表示ifa:type=='char'&& a:type!=0echon' "'. @@ .'"'elseechon @@
endif"状態復帰let&selection= sel_save
let @@ = reg_save
endfu
コマンドでも同じことをやってみたい(オマケ)
せっかく作った処理なので、オペレータからだけではなく、コロンコマンドでも実行してみたいな、と思ったので、少し追記してみました。
コメントの「(★)」箇所が追加した部分です。
ユーザ定義コマンドの場合は、-count
を付けると [count] 指定できるようになるらしいのですが、-range
指定と同時に指定する場合はどのようにしたらいいのかまだわかっていません。
ザコが手探りしてる感満載の記事でしょう?
opfunc_test2.vim
"コマンド定義(★)com!-buffer-range Echorange call<SID>echorange('command',<line1>,<line2>)
nnoremap <silent> Z :setopfunc=<SID>echorange<CR>g@
vnoremap <silent> Z :<C-U>call<SID>echorange(visualmode(),1)<CR>fu!s:echorange(type,...)"状態退避&変更let sel_save =&selectionlet&selection="inclusive"let reg_save = @@
"コマンドから (★)ifa:type=='command'"開始・終了行を取得letfirst=a:1letlast=a:2"行範囲をヤンクsilent exe first.','.last.'y'"コマンド以外で、可変長引数あり(vmapから)elseifa:0"範囲再選択silent exe "normal! gv""開始・終了行を取得let line1 =line('.')| normal!olet line2 =line('.')| normal!oletfirst= line1<line2? line1:line2
letlast= line1<line2? line2:line1
"選択範囲をヤンクsilent exe "normal! gvy""上記以外、g@(nmapから)else"モーションの開始・終了マークから、開始・終了行を取得letfirst=line("'[")letlast=line("']")"モーション範囲をヤンクifa:type=='line'silent exe "normal! '[V']y"elsesilent exe "normal! `[v`]y"endifendif"タイプ、および、開始・終了行を表示echona:typeechon': '.first.' - '.lastechon"\n""レジスタ内容を表示ifa:type=='char'&& a:type!=0echon' "'. @@ .'"'elseechon @@
endif"状態復帰let&selection= sel_save
let @@ = reg_save
endfu
応用例
上記の例では、さすがに何の役にも立ちませんので、もう少し意味ありげな使い方を模索してみました。
・・・といっても、範囲をソートするだけで、:'<,'>!sort
と変わらないため、相変わらず実用性は皆無です。
一応、テンプレとして echorange() は残しつつ、sortrange を追加して、共通部分を切り出しました。
今更ですが、可変引数については、:h a:0
または、を見るとわかります。
:h ...
でも同じですし、a:000
でもヘルプ同じです。
後は、Leaderキーを使ったマップにしてあります。
:h <leader>
、:h <Leader>
、:h mapleader
を参照の事。
opfunc_test3.vim
"範囲を表示するだけcom!-buffer-range Echorange call<SID>echorange('command',<line1>,<line2>)
nnoremap <silent><LocalLeader>z :setopfunc=<SID>echorange<CR>g@
vnoremap <silent><LocalLeader>z :<C-U>call<SID>echorange(visualmode(),1)<CR>fu!s:echorange(type,...) "{{{calls:fuStart(a:type,a:000)"タイプ、および、開始・終了行を表示echona:typeechon': '.s:first.' - '.s:lastechon"\n""レジスタ内容を表示ifa:type=='char'&& a:type!=0echon' "'. @@ .'"'elseechon @@
endifcalls:fuEnd()
endfu "}}}"範囲をソートするcom!-buffer-range Sortrange call<SID>sortrange('command',<line1>,<line2>)
nnoremap <silent><LocalLeader>s :setopfunc=<SID>sortrange<CR>g@
vnoremap <silent><LocalLeader>s :<C-U>call<SID>sortrange(visualmode(),1)<CR>fu!s:sortrange(type,...) "{{{calls:fuStart(a:type,a:000)"行単位以外は何もしないifa:type!='line'&& a:type!='V'&& a:type!='command'returnendif
exe s:first.','.s:last.'!sort'"3"2"1calls:fuEnd()
endfu "}}}"--------------------------------------------------------------------------------"以降は共通処理{{{"--------------------------------------------------------------------------------fus:fuStart(type,pList) "{{{setlazyredraw"状態退避&変更lets:save_wi=winsaveview()lets:sel_save=&selectionlet&selection="inclusive"lets:reg_save= @@
"コマンドからifa:type=='command'"開始・終了行を取得lets:first=a:pList[0]lets:last=a:pList[1]"行範囲をヤンクsilent exe s:first.','.s:last.'y'"コマンド以外で、可変長引数あり(vmapから)elseifa:type=='v'||a:type=='V'||a:type=="<c-v>""範囲再選択silent exe "normal! gv""開始・終了行を取得let line1 =line('.')| normal!olet line2 =line('.')| normal!olets:first= line1<line2? line1:line2
lets:last= line1<line2? line2:line1
"選択範囲をヤンクsilent exe "normal! gvy""上記以外、g@(nmapから)else"モーションの開始・終了マークから、開始・終了行を取得lets:first=line("'[")lets:last=line("']")"モーション範囲をヤンクifa:type=='line'silent exe "normal! '[V']y"elsesilent exe "normal! `[v`]y"endifendifendf "}}}fus:fuEnd() "{{{"状態復帰let&selection=s:sel_savelet @@ =s:reg_savecallwinrestview(s:save_wi)setnolazyredrawendf "}}}"}}}
最後に
任意の箇所で、任意のテキスト範囲に対して、正規表現使いたい放題で任意の処理を、任意のタイミングで発動できる、ってすごいことですよね。
任意のテキスト範囲(テキストオブジェクトですな)を追加するプラグインもあるので、そのうち手を出してみたいですねぇ。