概要
前回の記事「Vimテキストオブジェクトの自作(手習い)」の続きです。
テキストオブジェクトをプラグインとして、一から自作してみようという試みです。
目的は以下の2点でした。
- テキストオブジェクトの1文字目としての s を実現する。(surround.vim 的な)
- テキストオブジェクトの2文字目としての % を実現する。(:h matchpairs参照)
大体目的は達成したので、記事としてまとめてみました。
作成初期の紆余曲折については、前回の記事を参照。
今回は完成品のお披露目です。
前回記事終了時から色々と弄りましたが、修正過程まで書き込むと非常に冗長になるという前回記事の反省を踏まえて、今回は機能説明だけにします。
環境
- Microsoft Windows [Version 10.0.19042.685]
- GVIM 8.2.1287 (kaoriya 2020/07/24)
最初に
この記事の作成に至った経緯は…
- surround.vim ( + repeat.vim )
- vim-surround
- vim-operator-user + vim-operator-surround
- vim-sandwich
これらの素晴らしいものを、使ってみたい!
でもプラグインマネージャ入れるのはいやだ。(ネットに繋がってない場所で使うことの方が多い)
そうだ自分で作ってみよう!
・・・というわけではありませんでした。
実のところ、機能としては surround.vim の記事を読むことがあったのですが、便利なのかなー、という程度の認識でした。今回、「テキストオブジェクトを作る」という手段のため、目的として選んでみました。
どれも実際には使ったことはありませんが、自分で作ってみると、これは便利な物なのかもなー、という感じです。ということで、使い勝手については実感を伴わない伝聞でしか知らないので、先達の方々と同じ振る舞いをするとは限りません。
むしろ自分でもso %
としてチョロチョロ動かしている程度しか使っていない(!)ので、ヘンな動きも残っているかもしれません。そこの辺りは、実験品という扱いで一つよろしくお願いします。
上記の先達の方々は、それぞれ独自のポリシーにより多少の方言があるようです。もちろん、物によってはカスタマイズにより好みに変えられるようです。
一応、私の脳内は、最初に知った surround.vim の文法の影響を最も強く受けているのですが、私のヤツの場合は、トリガーになるキーストロークを変えられる程度の、カスタマイズ性というのも烏滸がましい程度の機能しか有しておりません。
特徴
- 一応、特徴(だと思っている)のは当初の目的でもあり、プラグイン名にも付けた、マッチペアを扱うテキストオブジェクトです。
- 今更特徴ともいえないのかもしれませんが、自前のドットリピートに対応しています。
- repeat.vim、vim-operator-user、vim-textobj-user に依存しません。
- ファイルを一個置くだけで設定完了。vimrcに何かを追記したりする必要もありません。
機能説明
以下のセクションで説明するオペレータ、テキストオブジェクト等が追加されます。
最初から言っておきますが、特に目新しいものはありません。
オペレータ
以下のオペレータがノーマルモードに追加されます。
キー割り当ては、後述する辞書変数による外部定義で変更することが可能です。
キー | 機能 |
---|---|
cs{surround1}{surround2} | 括り文字{surround1}から{surround2}への変更 |
ds{surround} | 括り文字{surround}の削除 |
ys{textobject}{surround} | {textobject}の前後に{surround}を追加 |
yss{surround} | 該当行に括り文字{surround}を追加 (ysil{surround}と同義) |
gus{surround} | 括り文字の小文字変換(※1) |
gUs{surround} | 括り文字の大文字変換(※1) |
g~s{surround} | 括り文字の大文字小文字トグル変換(※1) |
gz{textobject} | {textobject}を対象に全角→半角変換(※2) |
gZ{textobject} | {textobject}を対象に半角→全角変換(※2) |
gzs{surround} | 括り文字{surround}の半角→全角変換(※2) |
gZs{surround} | 括り文字{surround}の全角→半角変換(※2) |
- ※1.{surround} の「t」にしか意味がありません。(出来るからやってみただけで、使い道があるとは思っていない。。)
- ※2.
hz_ja.vim
(kaoriya版に標準付属)に依存しています。hz_ja.vim
が存在しない場合、該当機能は有効になりません。
「gz」や「gZ」はともかく、果たして「gzs」や「gZs」に意味はあるのか? と思われるかもしれませんが、後述する {surround} の「%」と組み合わせて、各種全角括弧を扱ったりすることができるはずです。
以下のオペレータがビジュアルモードに追加されます。
こちらのキー割り当ても、後述する辞書変数による外部定義で変更することが可能です。
キー | 機能 |
---|---|
{visual}S{surround} | {visual}の文字列の前後に{surround}を追加 |
{linewisevisual}S{surround} | {linewisevisual}の行の前後の行として{surround}を追加 |
テキストオブジェクト
{textobject}
以下の{textobject}が追加されます。
キー割り当てを辞書変数による外部定義で変更することが可能です。(al,ilは除く)
キー | 機能 |
---|---|
a% | 'matchpairs'の組と、その組に挟まれた文字列 |
i% | 'matchpairs'の組に挟まれた文字列 |
af{char} | {char}とその間に挟まれた文字列 |
if{char} | {char}に挟まれた文字列 |
al | カーソル行の最初の文字から改行 |
il | カーソル行の最初の非空白文字から行末 |
「a%」「i%」・・・マッチペアとして定義されている文字の間にカーソルがあるとき、i%
でその内側を、a%
でマッチペアを含んで、…という感じに振舞います。
全角括弧を対象にしたかったので、オペレータの内部で一時的に以下のmpsを勝手に追加して動作しています。
set mps+=(:),「:」,{:},[:],【:】,『:』,<:>,≪:≫,《:》
vimrcでset mps+=❝:❞
等としていれば、それも対象になります。
・・・が、もし上記のペアから除外したい場合は、直接スクリプトを弄ってもらうしかないです。(このドキュメントを書いている、今この瞬間までここを外部定義にするという発想がなかった)
「af」「if」・・・たぶん、vim-textobj-betweenと同じです。任意の1文字に挟まれた範囲を扱います。
実は、{surround}として入力したキーがテキストオブジェクトの2文字目に該当しない文字だった場合、「その文字に挟まれた間」として動作させてしまっていたのですが、被った場合に使えないので明確に「その文字に挟まれた間」としての機能を切り出しました。
キー割り当ては外部定義で変更可能です。
「al」「il」・・・これはテキストオブジェクトという程のものではなく、実は単なるマッピングです。
オペレータのモーション(omap)としてだけではなく、ヴィジュアルモードのモーション(vmap)としても定義してあるので、「val」や「vil」として使用することも可能です。
キー割り当ては固定になります。
{surround}
「a{x}」と「i{x}」が、対で存在しているテキストオブジェクトに対して、「s{x}」として使用することが出来るようになります。
「i{x}」の範囲から「a{x}」の範囲で広がる部分が「s{x}」の対象になります。
※変更元や削除対象として指定可能です。変更先や追加対象の{surround}としては使えません。
上記の{textobject}の追加により、独自の{surround}として、以下のキーが追加されます。
キー | 機能 |
---|---|
% | 'matchpairs'の組そのもの |
f{char} | カーソル位置の左右に存在する{char} |
「s%」の使い方は、例えばcs%"
とすれば、「あいう」
を、"あいう"
に変更出来て、そのまま次は【かきく】
の上にカーソルがあるときにドットリピートすると"かきく"
に変えられるわけですな。
gzs%
で全角括弧を半角括弧に変えたりできます。(hz_ja.vim
が必要)
ちなみに、vmapにa{x}とi{x}を定義するだけでも、s{x}が使えるようになってしまうという仕組み上、「al」と「il」に対応して自動的に「sl」が有効になっていますが、「行頭の空白文字と、行末の改行」が対象になるという、使いどころのないものになってしまっています。
もちろん、標準的なテキストオブジェクト(:h object-select
参照)で"a"と"i"が対で存在しているものもすべて使えます。
いかにも「ザ・サラウンド!」というものから、イマイチ使い道がなさそうなものまでありますが、標準テキストオブジェクトによって使えるようになるものを列挙しておきます。
キー | 機能 |
---|---|
] [ |
'['と']'を対象にする |
) ( b |
'('と')'を対象にする |
> < |
'<'と'>'を対象にする |
t |
'<xxx>' と'</xxx>' を対象にする |
} { B |
'{'と'}'を対象にする |
" ' ` |
引用符を対象にする |
w | 単語(word:英数とアンダースコアの連続)の前後に続く空白部分のみを対象にする |
W | 単語(WORD:非空白文字の連続)の前後に続く空白部分のみを対象にする |
s | 文の前後を対象にする yssのみ、カーソル行を対象にする |
p |
段落の前後を対象にする。通常は段落の後ろにある空行部分。(EOFとの間に空行がない段落は、段落の前にある空行部分が対象になる) |
特別なことはしていないつもりなのですが、pだけまともに動作しません。興味深いです。暇になったらじっくり調べてみたい。
内部的にはva{x}
とvi{x}
を行って範囲の差分を取得していたのですが、その時に行単位の選択に対処できていませんでした。vap
とvip
だけが行単位の選択になっていたため、pだけ動かない、ということになってました。
確かに:h ap
や:h ip
を読むと、ビジュアルモードで使われたときは、行単位になります。
と書かれています。勉強になりますね。
その他追加されるマッピング
以下はオペレータでもテキストオブジェクトでもなく、単なるマッピングです。
検索パターン系マッピング
テキストオブジェクトとして検索パターンをどうにか利用しようと考えたのですが、マッピングでできるじゃん、というレベルの内容に落ち付いてしまいました。
検索パターンで正規表現を使えば、色々なものにマッチする対象をドットリピートで次々に置き換えたり、削除したりできるのは何気に便利ではないかと思うのです。
キー | 機能 |
---|---|
cn | カーソルの前方に存在する最終検索パターンをレジスタに入れずに削除して、挿入を始める |
cN | カーソルの後方に存在する最終検索パターンをレジスタに入れずに削除して、挿入を始める |
dn | カーソルの前方に存在する最終検索パターンをレジスタに入れずに削除する |
dN | カーソルの後方に存在する最終検索パターンをレジスタに入れずに削除する |
crr | カーソルの前方に存在する最終検索パターンをレジスタ内容と置き換える |
cRr cRR |
カーソルの後方に存在する最終検索パターンをレジスタ内容と置き換える |
crc | カーソルの前方に存在する最終検索パターンをクリップボード内容と置き換える |
cRc cRC |
カーソルの後方に存在する最終検索パターンをクリップボード内容と置き換える |
※カーソルがマッチの上にある時は、それを対象にします
crr は「change replace register」とでも覚えてください。
crc は「change replace clipboard」とでも覚えてください。
英語としてどうなのかとかは気にしたら負けです。
クリップボード系マッピング
クリップボードを少し身近にします。レジスタとクリップボードを一緒の扱いにしてしまうのは個人的に嫌なので、こんな風にしてみました。というか元々vimrcに書いていたのを持ってきて、ちょっと手直しました。
キー | 機能 |
---|---|
gy{motion} | {motion}をクリップボードへコピーする |
{visual}gy | {visual}をクリップボードへコピーする |
gyy | 非空白文字~行末までをコピーをクリップボードへコピーする |
gY | 行頭~行末までをコピーをクリップボードへコピーする |
不要なら削除してください。
:verbose map gy
等とすれば、どこで定義されているのか見ることができるので、サクッとコメントアウトすればOK。
gyの{motion}には、当プラグインにて追加されるal
やil
も使えるのでgyal
やgyil
という使い方もできます。
・・・まぁ、gyal
はgY
と同じですし、gyil
はgyy
と同じなのですが。
gyal
とgY
は、同じではありませんでした。
gY
は行頭~行末までをクリップボードにコピーしますが、gyal
は行頭~改行までをクリップボードにコピーします。
個人的にはgyip
が高評価です。
vip"*y
だとか、[jV]"*y
(改行コードが一つ余分に入る)だとかよりも、ずっと押しやすいです。
外部定義
g:matchpairsand_beyondLine
辞書変数で、デフォルトのキーストロークを変更することができます。
マップ名 | デフォルトキー | 機能 |
---|---|---|
(MatchpairsandChangeSand) | cs | 囲み置換 |
(MatchpairsandDeleteSand) | ds | 囲み削除 |
(MatchpairsandAddSand) | ys | 囲み追加 |
(MatchpairsandAddSandVisual) | S | 囲み追加(visual) |
(MatchpairsandLowerCaseSand) | gus | 小文字化 |
(MatchpairsandUpperCaseSand) | gUs | 大文字化 |
(MatchpairsandToggleCaseSand) | g~s | 大文字小文字トグル |
(MatchpairsandHannkaku) | gz | 半角化 |
(MatchpairsandZenkaku) | gZ | 全角化 |
(MatchpairsandInnerMps) | i% | マッチペア |
(MatchpairsandAroundMps) | a% | マッチペア |
(MatchpairsandInnerMpsVisual) | i% | マッチペア(visual) |
(MatchpairsandAroundMpsVisual) | a% | マッチペア(visual) |
(MatchpairsandInnerFind) | if | 任意1文字 |
(MatchpairsandAroundFind) | af | 任意1文字 |
(MatchpairsandInnerFindVisual) | if | 任意1文字(visual) |
(MatchpairsandAroundFindVisual) | af | 任意1文字(visual) |
例えば、以下のようにすることで、デフォルトではa%
,i%
,s%
だったものが、それぞれ、am
,im
,sm
に置き換わります。
let g:matchpairsand_keys = {
\ '<Plug>(MatchpairsandInnerMps)' : 'im'
\,'<Plug>(MatchpairsandAroundMps)' : 'am'
\,'<Plug>(MatchpairsandInnerMpsVisual)' : 'im'
\,'<Plug>(MatchpairsandAroundMpsVisual)' : 'am'
\}
ちなみに、デフォルトは内部的にこうなっています。(s:matchpairsand_keys)
let s:matchpairsand_keys = {
\ '<Plug>(MatchpairsandChangeSand)' : 'cs'
\,'<Plug>(MatchpairsandDeleteSand)' : 'ds'
\,'<Plug>(MatchpairsandAddSand)' : 'ys'
\,'<Plug>(MatchpairsandAddSandVisual)' : 'S'
\,'<Plug>(MatchpairsandLowerCaseSand)' : 'gus'
\,'<Plug>(MatchpairsandUpperCaseSand)' : 'gUs'
\,'<Plug>(MatchpairsandToggleCaseSand)' : 'g~s'
\,'<Plug>(MatchpairsandHannkaku)' : 'gz'
\,'<Plug>(MatchpairsandZenkaku)' : 'gZ'
\,'<Plug>(MatchpairsandInnerMps)' : 'i%'
\,'<Plug>(MatchpairsandAroundMps)' : 'a%'
\,'<Plug>(MatchpairsandInnerMpsVisual)' : 'i%'
\,'<Plug>(MatchpairsandAroundMpsVisual)' : 'a%'
\,'<Plug>(MatchpairsandInnerFind)' : 'if'
\,'<Plug>(MatchpairsandAroundFind)' : 'af'
\,'<Plug>(MatchpairsandInnerFindVisual)' : 'if'
\,'<Plug>(MatchpairsandAroundFindVisual)' : 'af'
\} "}}}
g:matchpairsand_keys
この値が 0 の場合、{surround}はカーソル行内のみが対象になります。
1 を設定した場合は、カーソル行の外側にある囲み文字も対象になります。
デフォルトは 1 です。
カーソル行以外が編集対象になる場合は、囲みの開始位置と終了位置を「行→編集箇所」の順で、一瞬ヴィジュアル選択させることで注意喚起するようにしてあります。
g:matchpairsand_wait
編集対象になる{surround}に一瞬カーソルを合わせる時に点滅する時間をミリ秒単位で指定します。
デフォルトは50です。
{surround}が複数行にまたがる場合は、指定した時間で2度点滅します。
0を指定すると、カーソル移動を行いません。
設定方法
MatchPairSand.vim
を、'runtimepath'配下のpluginディレクトリに置いてください。
:h vimfiles
参照の事。
以上。
本体
" vi: set et ts=4 sts=4 sw=4 fdm=marker tw=0 :
"Vim global plugin for matchpair text object.
"Last Change:2021/01/15 10:57:50.
"Maintainer:azuwai
"License:This file is placed in the public domain.
scriptencoding utf-8
"二重ロード避け
if exists("g:loaded_matchpairsand")
finish
endif
let g:loaded_matchpairsand = 1
"互換性オプションを退避して規定値に設定
let s:save_cpo = &cpo
set cpo&vim
"マッピングがタイムアウトしにくくなるように(i%やa%はUMPCを片手持ちしていると1秒以内に打つのが難しい)
"・・・プラグイン内でグローバルな設定を変えるのは、あまりお行儀がよろしくないのでしょうか?
set timeout timeoutlen=3000 ttimeoutlen=100
"設定{{{
if exists("g:matchpairsand_keys") == 0
"ユーザ定義のサンプル。
"こんな感じでデフォルト設定を上書きできます。
"「i% と a% なんて、Shift が必要だし打ちづらいから、im と am に定義しなおすぜ!」
"という場合、以下のコメントアウトを外してもいいし、自分の vimrc で定義してもいい。
"※ただし、[M]atch[P]air だからと言って ip や ap 等は既存のテキストオブジェクトなので避けるのが吉。
"let g:matchpairsand_keys = {
" \ '<Plug>(MatchpairsandInnerMps)' : 'im'
" \,'<Plug>(MatchpairsandAroundMps)' : 'am'
" \,'<Plug>(MatchpairsandInnerMpsVisual)' : 'im'
" \,'<Plug>(MatchpairsandAroundMpsVisual)' : 'am'
" \}
endif
"行跨ぎするような囲い文字を対象にするかどうか
if exists("g:matchpairsand_beyondLine") ==0
let g:matchpairsand_beyondLine = 1
endif
if exists("g:matchpairsand_wait") == 0
let g:matchpairsand_wait = 50
endif
"}}}
"マッピング{{{
"単純キーマップ追加{{{
"i/ a/ をテキストオブジェクトとして定義しようとしたが、やりたいこと的にこれで事足りそうだったので。
"※ i/ と a/ の違いをどう持たせるのか思いつかなかった、というのもある。
"正規表現でマッチしたパターンを次々とドットで繰り返せるのが便利そう。
nnoremap cn "_cgn
nnoremap cN "_cgN
nnoremap dn "_dgn
nnoremap dN "_dgN
"検索パターンをレジスタ内容で置き換える
nnoremap crr "_cgn<C-R>"<Esc>
nnoremap cRr "_cgN<C-R>"<Esc>
nnoremap cRR "_cgN<C-R>"<Esc>
"検索パターンをクリップボード内容で置き換える
nnoremap crc "_cgn<C-R>*<Esc>
nnoremap cRc "_cgN<C-R>*<Esc>
nnoremap cRC "_cgN<C-R>*<Esc>
"クリップボードを少し身近に(でも一緒にするのは嫌)
"gyip 等だけではなく、当プラグインのテキストオブジェクトとの合わせ技で、 gyil や gyal も発動する。
"※ gc や gd はバッティングリスクの割に利便性もイマイチ感じられないので無し。
nnoremap gy "*y
vnoremap gy "*y
nnoremap gyy ^"*y$
nnoremap gY 0"*y$
"}}}
"テキストオブジェクトのマッピング(ax,ix) "{{{
"1行
onoremap <silent> al :<C-U>normal!0v$<CR>
vnoremap al 0o$
"インデント除いた1行
onoremap <silent> il :<C-U>normal! ^vg_<CR>
vnoremap il ^og_
"}}}
"デフォルトのマッピング定義(「g:matchpairsand_keys」を定義することで上書き可能){{{
"要素「マッピングインターフェイス名、キーストローク」
let s:matchpairsand_keys = {
\ '<Plug>(MatchpairsandChangeSand)' : 'cs'
\,'<Plug>(MatchpairsandDeleteSand)' : 'ds'
\,'<Plug>(MatchpairsandAddSand)' : 'ys'
\,'<Plug>(MatchpairsandAddSandVisual)' : 'S'
\,'<Plug>(MatchpairsandLowerCaseSand)' : 'gus'
\,'<Plug>(MatchpairsandUpperCaseSand)' : 'gUs'
\,'<Plug>(MatchpairsandToggleCaseSand)' : 'g~s'
\,'<Plug>(MatchpairsandHannkaku)' : 'gz'
\,'<Plug>(MatchpairsandZenkaku)' : 'gZ'
\,'<Plug>(MatchpairsandInnerMps)' : 'i%'
\,'<Plug>(MatchpairsandAroundMps)' : 'a%'
\,'<Plug>(MatchpairsandInnerMpsVisual)' : 'i%'
\,'<Plug>(MatchpairsandAroundMpsVisual)' : 'a%'
\,'<Plug>(MatchpairsandInnerFind)' : 'if'
\,'<Plug>(MatchpairsandAroundFind)' : 'af'
\,'<Plug>(MatchpairsandInnerFindVisual)' : 'if'
\,'<Plug>(MatchpairsandAroundFindVisual)' : 'af'
\} "}}}
"マッピングインターフェイス{{{
"要素「マッピングインターフェイス名、配列[モード、関数呼び出し+第1引数、第3引数、受け取る文字数]」
" ※第2引数は自動的にキーストロークを設定する
let s:matchpairsand_futures = {
\ '<Plug>(MatchpairsandChangeSand)' : [ 'n' , ':<C-U>call <SID>cSurround(v:false' , '' , 0 ]
\,'<Plug>(MatchpairsandDeleteSand)' : [ 'n' , ':<C-U>call <SID>dSurround(v:false' , '' , 0 ]
\,'<Plug>(MatchpairsandAddSand)' : [ 'n' , ':<C-U>call <SID>ySurround(v:false' , '' , 0 ]
\,'<Plug>(MatchpairsandAddSandVisual)' : [ 'v' , ':<C-U>call <SID>sSurround(v:false' , '' , 0 ]
\,'<Plug>(MatchpairsandLowerCaseSand)' : [ 'n' , ':<C-U>call <SID>guSurround(v:false' , 'u' , 0 ]
\,'<Plug>(MatchpairsandUpperCaseSand)' : [ 'n' , ':<C-U>call <SID>guSurround(v:false' , 'U' , 0 ]
\,'<Plug>(MatchpairsandToggleCaseSand)' : [ 'n' , ':<C-U>call <SID>guSurround(v:false' , '~' , 0 ]
\,'<Plug>(MatchpairsandHannkaku)' : [ 'n' , ':<C-U>call <SID>gzOperator(v:false' , 'gHL' , 0 ]
\,'<Plug>(MatchpairsandZenkaku)' : [ 'n' , ':<C-U>call <SID>gzOperator(v:false' , 'gZL' , 0 ]
\,'<Plug>(MatchpairsandInnerMps)' : [ 'o' , ':<C-U>call <SID>mpsObject(v:false' , 'i' , 0 ]
\,'<Plug>(MatchpairsandAroundMps)' : [ 'o' , ':<C-U>call <SID>mpsObject(v:false' , 'a' , 0 ]
\,'<Plug>(MatchpairsandInnerMpsVisual)' : [ 'v' , ':<C-U>call <SID>mpsObject(v:false' , 'i' , 0 ]
\,'<Plug>(MatchpairsandAroundMpsVisual)' : [ 'v' , ':<C-U>call <SID>mpsObject(v:false' , 'a' , 0 ]
\,'<Plug>(MatchpairsandInnerFind)' : [ 'o' , ':<C-U>call <SID>findObject(v:false' , 'i' , 0 ]
\,'<Plug>(MatchpairsandAroundFind)' : [ 'o' , ':<C-U>call <SID>findObject(v:false' , 'a' , 0 ]
\,'<Plug>(MatchpairsandInnerFindVisual)' : [ 'v' , ':<C-U>call <SID>findObject(v:false' , 'i' , 1 ]
\,'<Plug>(MatchpairsandAroundFindVisual)' : [ 'v' , ':<C-U>call <SID>findObject(v:false' , 'a' , 1 ]
\} "}}}
"リピート対応{{{
nnoremap <silent> . :<C-U>call <SID>dotRepeat()<CR>
nnoremap <silent> u :<C-U>call <SID>undoRepeat("u")<CR>
nnoremap <silent> U :<C-U>call <SID>undoRepeat("U")<CR>
"}}}
"Vim起動時のマッピング追加{{{
au VimEnter * call <SID>map_init()
function! s:map_init()
"ユーザー定義を優先的に設定
if exists("g:matchpairsand_keys")
call <SID>map_set(g:matchpairsand_keys)
endif
"デフォルト定義を設定
call <SID>map_set(s:matchpairsand_keys)
endfunction
"}}}
"}}}
"イベント{{{
au TextChanged,TextChangedI,TextChangedP * 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:map_set(dict)
for mapifs in keys(a:dict)
if !exists("*HzjaConvert")
if stridx(s:matchpairsand_futures[mapifs][1] ,'gzOperator') != -1
continue
endif
endif
if !hasmapto(mapifs)
"未定義の場合のみ設定する。(ユーザー定義を上書きしない)
"定義されているキーストロークを取得し、マッピングインターフェイスへマップ
let keystroke = a:dict[mapifs]
exec s:matchpairsand_futures[mapifs][0].'map <unique> ' . keystroke . ' ' . mapifs
"マッピングインターフェイスから関数へのマッピングを実施。キーストロークを引数に埋め込む。
"exec s:matchpairsand_futures[mapifs][0].'noremap <silent> ' . keystroke . ' ' .
exec s:matchpairsand_futures[mapifs][0].'noremap <silent> ' . mapifs . ' ' .
\ s:matchpairsand_futures[mapifs][1] . ',"' . keystroke . '","' .
\ s:matchpairsand_futures[mapifs][2] . '")<CR>'
endif
endfor
endf "}}}
"オペレータ{{{
fu! s:cSurround(dotRep, keystroke, ...) abort "{{{
"ドットリピート用に関数名を保持
let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
"echom "start!! " . b:dotRepeatFunc
let s:cmd = a:keystroke
let b:keystroke = a:keystroke
try
set lazyredraw
let wi_save = winsaveview()
"囲い部分の範囲を取得
if s:getSurround(a:dotRep)
return
endif
"ドットリピート時は再取得しない
if a:dotRep == v:false
"変更後の囲い文字を取得
call s:getAfter()
endif
try
let ai_save = &ai | setl noai
let ww_save = &ww | let &ww = "h,l"
"囲い終わり箇所の置き換え
if s:aEnd != s:iEnd
call setpos('.',s:iEnd) | normal! lv
call setpos('.',s:aEnd)
exec "normal! c".b:afterEnd
else
call setpos('.',s:iEnd) | exec "normal! a".b:afterEnd
endif
"囲い始め箇所の置き換え
if s:aStart != s:iStart
call setpos('.',s:aStart) | normal! v
call setpos('.',s:iStart) | normal! h
exec "normal! c".b:afterStart
else
call setpos('.',s:iStart) | exec "normal! i".b:afterStart
endif
finally
let &ai = ai_save
let &ww = ww_save
endtry
"自身の終了時にイベントで-1されるため、2を設定
let b:dotReady = 2
catch /InvalidlInput/
return
finally
set nolazyredraw
call winrestview(wi_save)
redraw! | echo ""
endtry
endf "}}}
fu! s:dSurround(dotRep, keystroke, ...) abort "{{{
"ドットリピート用に関数名を保持
let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
"echom "start!! " . b:dotRepeatFunc
let s:cmd = a:keystroke
let b:keystroke = a:keystroke
try
set lazyredraw
let wi_save = winsaveview()
"囲い部分の範囲を取得
if s:getSurround(a:dotRep)
return
endif
try
let ai_save = &ai | setl noai
let ww_save = &ww | let &ww = "h,l"
"囲い終わり箇所を削除
if s:iEnd != s:aEnd
call setpos('.',s:iEnd) | normal! lv
call setpos('.',s:aEnd) | normal! d
endif
"囲い始め箇所を削除
if s:aStart != s:iStart
call setpos('.',s:aStart) | normal! v
call setpos('.',s:iStart) | normal! hd
endif
finally
let &ai = ai_save
let &ww = ww_save
endtry
"自身の終了時にイベントで-1されるため、2を設定
let b:dotReady = 2
catch /InvalidlInput/
return
finally
set nolazyredraw
call winrestview(wi_save)
redraw! | echo ""
endtry
endfu "}}}
fu! s:ySurround(dotRep, keystroke, ...) abort "{{{
"ドットリピート用に関数名を保持
let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
"echom "start!! " . b:dotRepeatFunc
let s:cmd = a:keystroke
let b:keystroke = a:keystroke
try
set lazyredraw
let wi_save = winsaveview()
"囲う部分のテキストオブジェクト部分を取得
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
catch /InvalidlInput/
return
finally
set nolazyredraw
call winrestview(wi_save)
redraw! | echo ""
endtry
endf "}}}
fu! s:sSurround(dotRep, keystroke, ...) abort "{{{
"ドットリピート用に関数名を保持
let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
"echom "start!! " . b:dotRepeatFunc
let s:cmd = a:keystroke
let b:keystroke = a:keystroke
try
set lazyredraw
let wi_save = winsaveview()
"let &selection = "inclusive" "&selectionを変えれば、vやVの時のSでも1行単位にできるかも?
"yssを使えば良いので、あまり必要性を感じないが…
if a:dotRep == v:false
"再選択
normal! gv
let b:sSuMode = mode()
"囲い範囲を取得
let iEnd = getcurpos() | normal! o
let iStart = getcurpos() | exec "normal! ". b:sSuMode
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
try
let ai_save = &ai | setl noai
let ww_save = &ww | let &ww = "h,l"
"setl paste
let iStart = getcurpos()
"囲い開始
if b:sSuMode==# 'v'
exec "normal! i".b:afterStart."\<ESC>"
elseif b:sSuMode==# 'V'
exec "normal! I".b:afterStart."\<CR>"
endif
let wk = getcurpos()
if 0 < b:vDown
let wk[1] += b:vDown
let wk[2] -= 1
call setpos('.',wk)
endif
let wk[2] += b:vRight+1
call setpos('.',wk)
"囲い終わり
let iEnd = getcurpos()
if b:sSuMode==# 'v'
exec "normal! a".b:afterEnd
elseif b:sSuMode==# 'V'
"行単位ヴィジュアルモードだった場合は、成形しておく。
exec "normal! A\<CR>".b:afterEnd
call setpos('.',iStart) | normal! V
call setpos('.',iEnd) | normal! j=
endif
finally
let &ai = ai_save
let &ww = ww_save
"setl nopaste
endtry
"自身の終了時にイベントで-1されるため、2を設定
let b:dotReady = 2
catch /InvalidlInput/
return
finally
set nolazyredraw
call winrestview(wi_save)
redraw! | echo ""
endtry
endf "}}}
fu! s:guSurround(dotRep, keystroke, ...) abort "{{{
"ドットリピート用に関数名を保持
let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
"echom "start!! " . b:dotRepeatFunc
let s:cmd = a:keystroke
let b:keystroke = a:keystroke
let b:case = a:1
try
set lazyredraw
let wi_save = winsaveview()
"囲い部分の範囲を取得
if s:getSurround(a:dotRep)
return
endif
try
let ai_save = &ai | setl noai
let ww_save = &ww | let &ww = "h,l"
"囲い終わり箇所を処理
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
let &ww = ww_save
endtry
"自身の終了時にイベントで-1されるため、2を設定
let b:dotReady = 2
catch /InvalidlInput/
return
finally
set nolazyredraw
call winrestview(wi_save)
redraw! | echo ""
endtry
endfu "}}}
fu! s:gzOperator(dotRep, keystroke, ...) abort "{{{
"ドットリピート用に関数名を保持
let b:dotRepeatFunc=s:funcName(expand("<sfile>"))
"echom "start!! " . b:dotRepeatFunc
let s:cmd = a:keystroke
let b:keystroke = a:keystroke
try
set lazyredraw
let wi_save = winsaveview()
if a:dotRep == v:false
let b:case = a:1
let b:gzWhere = s:getchar()
endif
if b:gzWhere == 's'
"let s:cmd .= b:gzWhere
"囲い部分の範囲を取得
if s:getSurround(a:dotRep)
return
endif
try
let ai_save = &ai | setl noai
let ww_save = &ww | let &ww = "h,l"
"囲い終わり箇所を処理
call setpos('.',s:iEnd) | normal! lv
exec "call setpos('.',s:aEnd) | normal ".b:case
"囲い始め箇所を処理
call setpos('.',s:aStart) | normal! v
exec "call setpos('.',s:iStart) | normal h".b:case
finally
let &ai = ai_save
let &ww = ww_save
endtry
else
call s:getMotion(a:dotRep,b:gzWhere)
try
let ai_save = &ai | setl noai
call setpos('.',s:iStart) | normal! v
exec "call setpos('.',s:iEnd) | normal ".b:case
finally
let &ai = ai_save
endtry
endif
"自身の終了時にイベントで-1されるため、2を設定
let b:dotReady = 2
catch /InvalidlInput/
return
finally
set nolazyredraw
call winrestview(wi_save)
redraw! | echo ""
endtry
endfu "}}}
"}}}
"テキストオブジェクト{{{
"※dotRepは必ずfalse。keystrokeはs:getchar()時に使用。
fu! s:mpsObject(dotRep, keystroke, ...) abort "{{{
let i_a = a:1
normal! %
let wk1 = getcurpos() | normal! %
let wk2 = getcurpos()
if wk1 == wk2
"%でカーソルが移動しない場合は、何も選択しない
return
endif
let flg_reverse = 0
if wk1[1] > wk2[1]
"行が逆転
let flg_reverse = 1
elseif wk1[1] == wk2[1]
if wk1[2] > wk2[2]
"同一行だが、列が逆転
let flg_reverse = 1
endif
endif
if flg_reverse
"現在カーソルが開始側にいる場合、終了側へ移動させる。
normal! %
endif
if i_a == "a"
"外側を選択する
exec "normal! %v%"
elseif i_a == "i"
"内側を選択する
try
let ww_save = &ww | let &ww = "h,l"
"一つ左に戻って(行跨ぎ)、選択開始して、元の位置に戻る。
exec "normal! hvl"
"マッチペアへ移動して(%)、一つ右へ移動(行跨ぎ)して、選択範囲決定。
exec "normal! %l"
finally
let &ww = ww_save
endtry
normal! o
endif
endf "}}}
fu! s:findObject(dotRep, keystroke, ...) abort "{{{
let i_a = a:1
"let s:cmd = a:keystroke
try
set lazyredraw
"winsaveview()/winrestview()はしない。
"(vで選択する場合等、開始位置に戻るのはNG)
if exists("b:dotExe") == 0 || b:dotExe == 0
let b:findObjChar = s:getchar(0)
endif
if exists("b:findObjChar") == 0
"存在しない場合(前回getChar()でESCし、かつ、ドットリピート)
return 1
endif
let hitStart = 0
let hitEnd = 0
if i_a == 'i'
"内側の範囲を取得
let cur_save = getcurpos()
exec "normal! F". b:findObjChar
if getcurpos() != cur_save
let hitStart = 1
endif
normal! lv
let cur_save = getcurpos()
exec "normal! ,"
if getcurpos() != cur_save
let hitEnd = 1
endif
normal! h
if hitStart == 0 && hitEnd ==0
normal! v
endif
elseif i_a == 'a'
"外の範囲を取得
let cur_save = getcurpos()
exec "normal! F". b:findObjChar
if getcurpos() != cur_save
let hitStart = 1
endif
normal! v
let cur_save = getcurpos()
exec "normal! ,"
if getcurpos() != cur_save
let hitEnd = 1
endif
if hitStart == 0 && hitEnd ==0
normal! v
endif
endif
catch /InvalidlInput/
if exists("b:findObjChar")
unlet b:findObjChar
endif
"exec "normal! \<Esc>\<Esc>"
"exec ":stopi"
"return 1
finally
set nolazyredraw
endtry
endf "}}}
"}}}
"共通処理{{{
fu! s:getchar(...) "{{{
if exists("s:cmd")
redraw | echo s:cmd
endif
let wk = nr2char(getchar())
if wk !~ '[[:graph:]]'
redraw | echo '' | throw "InvalidlInput"
endif
if exists("s:cmd")
if a:0==0 || a:1==1
"可変引数無し、または、1が指定されている場合は、表示文字列として編集
"(編集したくないときに0を指定)
let s:cmd .= wk
endif
redraw | echo s:cmd
endif
return wk
endfu "}}}
"現在の囲い文字の入力を受け付けて、その範囲を取得する
fu! s:getSurround(dotRep) abort "{{{
try
"囲う対象を取得
if a:dotRep == v:true
"ドットリピート時は前回値を使用
let what = b:what
else
let what = s:getchar()
let b:what = what
endif
"すでにビジュアルモードの場合、モードを終了する。
if mode() ==? "v"
exec "normal! " . mode()
endif
"cmd文字列の変遷の確認用
"let s:dbg_cnt=0 | mes clear
"let s:dbg_cnt+=1 | redraw! | echom s:dbg_cnt . s:cmd | sleep 2
let wh2 = s:getCustomMotion("i",what)
let what = wh2[1]
"内側の範囲を取得
"※nomalに!を付けないことで、他で定義されたtext-objectを有効にする
exec "normal vi".what
if mode() ==# "v"
let s:iEnd = getcurpos() | normal! o
let s:iStart = getcurpos() | normal! ov
elseif mode() ==# "V"
normal! $
let s:iEnd = getcurpos() | normal! o
let s:iStart = getcurpos() | normal! oV
else
let s:iStart= getcurpos()
let s:iEnd= getcurpos()
endif
"外側の範囲を取得
exec "normal va".what
if mode() ==# "v"
let s:aEnd = getcurpos() | normal! o
let s:aStart = getcurpos() | normal! v
elseif mode() ==# "V"
normal $
let s:aEnd = getcurpos() | normal! o
let s:aStart = getcurpos() | normal! V
else
let s:aStart= getcurpos()
let s:aEnd= getcurpos()
endif
if s:iStart[1] != s:iEnd[1]
"同一行内ではない場合
if g:matchpairsand_beyondLine == 0
"行を超える設定ではない場合
return 1
endif
endif
"テキストオブジェクト(ix,ax)として見つからなかった場合
if s:aStart == s:aEnd
return 1
endif
"範囲確認用
"message clear
"echom "s:"s:aStart[1]. "/" . s:aStart[2]. " - " .s:iStart[1] . "/" . s:iStart[2]
"echom "e:"s:iEnd[1] . "/" . s:iEnd[2] . " - " .s:aEnd[1] . "/" . s:aEnd[2]
if g:matchpairsand_wait > 0
if s:aStart[1] != s:aEnd[1]
for i in range(1,2)
call setpos('.',s:aStart) | normal! V
redraw | exec "sleep ".g:matchpairsand_wait."m"
if s:aStart[1] == s:iStart[1]
call setpos('.',s:iStart) | normal! hv
endif
redraw | exec "sleep ".eval(g:matchpairsand_wait*2)."m"
exec "normal! ".mode()
redraw | exec "sleep ".g:matchpairsand_wait."m"
endfor
for i in range(1,2)
call setpos('.',s:aEnd) | normal! V
redraw | exec "sleep ".g:matchpairsand_wait."m"
if s:aEnd[1] == s:iEnd[1]
call setpos('.',s:iEnd) | normal! lv
endif
redraw | exec "sleep ".eval(g:matchpairsand_wait*2)."m"
exec "normal! ".mode()
redraw | exec "sleep ".g:matchpairsand_wait."m"
endfor
else
for i in range(1,2)
call setpos('.',s:aStart) | normal! v
call setpos('.',s:iStart) | normal! hv
redraw | exec "sleep ".g:matchpairsand_wait."m"
endfor
for i in range(1,2)
call setpos('.',s:aEnd) | normal! v
call setpos('.',s:iEnd) | normal! lv
redraw | exec "sleep ".g:matchpairsand_wait."m"
endfor
endif
endif
catch /InvalidlInput/
return 1
endtry
return 0
endf "}}}
"変更後/追加する囲い文字の入力を受け付ける
fu! s:getAfter() abort "{{{
try
let wk = s:getchar()
if wk ==# "b"
let wk = "("
elseif wk ==# "B"
let wk = "{"
endif
if wk == '<'
"タグの場合は、開始タグ・終了タグ
let l:tagStart =""
while wk != '>'
let l:tagStart .= wk
let wk = s:getchar()
endwhile
let l:tagStart .= wk
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
catch /InvalidlInput/
throw "InvalidlInput"
endtry
return 0
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 = ""
try
"モーション文字列を取得
if a:dotRep == v:true
"ドットリピート時は前回値を使用
let l:count = b:count
let l:where = b:where
let l:what = b:what
else
"リピート時以外は、テキストオブジェクトの入力を受け付ける
if a:0
"可変引数あり(gzOperator()から呼び出し)時
"すでに1文字目が入力されているとみなす
let wk = a:1
redraw | echo s:cmd
else
"可変引数なし(ySurround()から呼び出し)
let wk = s:getchar()
endif
while wk =~ "[0-9]"
"数字が入力され続ける間、[count]に保持
let l:count .= wk
let wk = s:getchar()
endwhile
if wk =~ "^[ia]$"
"テキストオブジェクトの場合
let l:where = wk
"もう1文字受け取ってテキストオブジェクトを完成させる
let wk = s:getchar()
if wk =~ "[0-9]"
"数字が入力された場合
if l:count == ""
"以前にcountが入力されていない場合、[count]として取得
while wk =~ "[0-9]"
let l:count .= wk
let wk = s:getchar()
endwhile
else
"以前にcountが入力されていた場合は、処理中断
throw "InvalidlInput"
endif
endif
endif
"受け取った文字を連結(カーソル移動の場合も含む)
let l:what = wk
let wh2 = s:getCustomMotion(l:where,l:what)
let l:where = wh2[0]
let l:what = wh2[1]
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
catch /InvalidlInput/
throw "InvalidlInput"
endtry
return 0
endf "}}}
fu! s:getCustomMotion(where,what) "{{{
if a:where == "" && a:what =="s"
"yss→ysil的な
let wh2 = [ "i", "l" ]
return wh2
endif
let wh2 = [ a:where, a:what ]
"自前のテキストオブジェクトで、2文字で完結しない場合のための処理
for mapifs in keys(s:matchpairsand_futures)
"入力を受け取る文字数
let len = s:matchpairsand_futures[mapifs][3]
"入力が必要なテキストオブジェクトの場合
if 0 < len
"キーストローク(デフォルト値)を取得
let keystroke = s:matchpairsand_keys[mapifs]
"キーストローク(ユーザ定義があれば)を取得
if exists("g:matchpairsand_keys")
let keystroke = get(g:matchpairsand_keys,mapifs,keystroke)
endif
"現在、入力中のテキストオブジェクトだった場合
if wh2[0].wh2[1] == keystroke
let wk = ""
"必要な文字数分を受け取って
while len(wk) < len
let wk .= s:getchar()
endwhile
"whatを置き換えて終了
let wh2[1] .= wk
break
endif
endif
endfor
return wh2
endf "}}}
"}}}
"ドットリピート関連{{{
"リピート
fu! s:dotRepeat() "{{{
let b:dotExe = 1
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.',"'.b:keystroke.'")' | redraw
else
silent normal! .
endif
let b:dotExe = 0
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
unlet s:save_cpo
最後に
完成してしまうとあっけないものです。
自分でも本格的に使うかどうかはわかりませんが、とりあえず作っている間は面白かったです。
おそらく、vim-textobj-userを使って、matchpairsにだけ対応するものを公開したほうが、スリムで利便性も高くて、利用してもらい甲斐もあるのでしょうが、今のところやる気はないです。