PerlやRubyの正規表現などをtext objectとして扱うためのplugin

Vim Advent Calendar 2012の9日目の記事です。
8日目は@kefir_さんによるvimと端末エミュレーションでした。

今日は、私の自作plugin、textobj-enclosedsyntaxをご紹介したいと思います。
記事のタイトルでは、わかりやすさのためPerlRuby正規表現などをtext objectとして扱うためのpluginと書きましたが、本pluginが扱う範囲はもう少し汎用的です。一文で説明すると本pluginの説明はこうです。
「任意のsyntaxで囲まれた範囲をtext objectとして扱うためのplugin」
文章で説明してもよくわかりませんね(汗
本pluginを作成するにあたっての動機からお話したいと思います。

plugin作成の動機

私はプログラミング言語Perlを使います。
Perlではマッチ演算子(m//)で囲まれた部分を正規表現として扱い、文字列の正規表現チェックを
以下の様な形で行うことができます。

if ($hoge =~ m/正規表現/) {
  # ...
}

このマッチ演算子(m//)で囲まれた部分をtext objectで扱いたいと思ったのが本plugin作成の動機です。

textobj-enclosedsyntaxの使い方

インストール方法

textobj-enclosedsyntaxはgithubに上げているので、VundleやNeoBundleなどでインストールするのが楽かと思います。kanaさん作のtextobj-userも必須なので合わせてインストールしてください。
NeoBundleであれば以下の様な感じでvimrcに書けばインストールできます。

  NeoBundle 'deris/vim-textobj-enclosedsyntax',
    \ { 'depends' : 'kana/vim-textobj-user' }

デフォルトでは、aq、iqにキーマッピングしています。
デフォルトのキーマッピングが気に入らない方は、他のtextobj-user pluginと同様、vimrcに以下のように記載すればキーマップを変えられます。

  " デフォルトキーマッピング無効
  let g:textobj_enclosedsyntax_no_default_key_mappings = 1

  " ax、ixにマッピングしたい場合
  omap ax <Plug>(textobj-enclosedsyntax-a)
  vmap ax <Plug>(textobj-enclosedsyntax-a)
  omap ix <Plug>(textobj-enclosedsyntax-i)
  vmap ix <Plug>(textobj-enclosedsyntax-i)
使い方

以降は、デフォルトキーマッピング(aq、iq)のままであることを前提として記載します。

例えばマッチ演算子(m//)内でdiqとすると、以下のようにマッチ演算子内の正規表現だけ削除されます。

if ($hoge =~ m/hoge/) {
  # ...
}
# diqとtypeすると↓
if ($hoge =~ m//) {
  # ...
}

例えばマッチ演算子(m//)内でdaqとすると、以下のようにマッチ演算子含めて正規表現が削除されます。

if ($hoge =~ m/hoge/) {
  # ...
}
# daqとtypeすると↓
if ($hoge =~ ) {
  # ...
}

マッチ演算子全体をtext objectとして扱うため、後ろにオプションをつけるとオプションも含めてtext objectとして扱われます。他にも(//)を(##)に変えても対応します。
例えばマッチ演算子(m//)内で、daqで以下の様な動作になります。

if ($hoge =~ m#hoge#gimoxx) {
  # ...
}
# daqとtypeすると↓
if ($hoge =~ ) {
  # ...
}
対応範囲

正規表現(マッチ演算子)だけに着目してご説明しましたが、他にもいくつかの範囲に対応しています。
今のところPerlRubyの以下の範囲にデフォルトで対応しています。

# Perlの場合
# マッチ演算子(//も)
m/hoge/
# q演算子(''も)
q(hoge)
# qq演算子(""も)
qq(hoge)
# qw演算子
qw(hoge)
# qx演算子(``も)
qx(hoge)
# qr演算子
qx(hoge)
# ヒアドキュメント
<<'EOS'
hoge
EOS
# Rubyの場合
# 正規表現リテラル
/hoge/
# 文字列リテラル
'hoge'
"hoge"
# コマンド出力
`hoge`
# %記法
%!hoge!
%Q!hoge!
%q!hoge!
%x!hoge!
%r!hoge!
%w!hoge!
%W!hoge!
%s!hoge!

類似pluginとの比較

ここまでを見て、ある程度なら既存pluginでも対応出来るのでは?と思われた方もいるかもしれません。
なので、簡単に既存pluginとの比較をします。(説明の都合上出来る出来ないで記載していますが、優劣をつけるという意味ではなく、違いを説明するためだけなので誤解なきよう。いずれも便利なpluginです。使い所は適材適所ですね。)

textobj-syntax

kanaさん作のtextobj-syntaxは同一syntaxをtext objectとして扱うためのpluginです。

syntaxハイライトされている部分をtext objectとして扱うことができます。

textobj-syntaxでは、囲みなど意識せず、同一syntaxが連続する場合はその部分すべてをtext objectの対象とします。
今回の動機であったPerlのマッチ演算子(m//)でtextobj-syntaxを使うと、マッチ演算子の初めの部分(m/)と、終りの部分(/)が同一syntaxではないため、一緒にtext objectとして扱われません。textobj-syntaxまた、囲みを意識することもありません。

textobj-enclosedsyntaxは、特定のsyntaxで囲まれた部分がtext objectの範囲になります。

textobj-between

thincaさん作のtextobj-betweenは、任意の文字の間に囲まれた範囲をtext objectとして扱うことができます。

例えば、for文内の「;」で囲まれた部分を選択することなどができます。

for (int i = 0; i < len; i++)
// 例えばlen上でdif;とタイプすると、「 i < len」の部分が削除される

単純なケースであれば、今回の動機になったPerlのマッチ演算子(m//)も対応可能です。

if ($hoge =~ m/hoge/) {
  # ...
}
# マッチ演算子のhogeの部分にカーソルがある状態で、dif/↓
if ($hoge =~ m//) {
  # ...
}

ただし、textobj-betweenでは、マッチ演算子(m//)をまとめて(m//とオプション含めて削除など)text objectとして扱うことができません。
また、マッチ演算子の中にスラッシュ(/)があった場合、マッチ演算子内を範囲として指定したいという今回の意図と異なる動作になってしまいます。

if ($hoge =~ m/ho \/ ge/) {
  # ...
}
# マッチ演算子のhoの部分にカーソルがある状態で、dif/↓
if ($hoge =~ m// ge/) {
  # ...
}

textobj-enclosedsyntaxはsyntaxを意識しますが、textobj-betweenはsyntaxを意識しません。当然お互いにできるところできないところは異なります。

pluginの仕組み

折角なので簡単に仕組みを。
実は、本pluginはカスタマイズ可能です(条件が揃えば他のfiletypeの囲みにも対応可能です)。

vimでは、構文ハイライトを正規表現パターンで指定しています。
例えばPerlの構文ハイライトのファイルは以下のファイルに記載されています。

$VIMRUNTIME/syntax/perl.vim

このファイルを使ってvimでは、各言語対応の構文ハイライトを行なっています。

本pluginはこの仕組みを利用しています。

例えば、Perlのマッチ演算子を構成する構文は以下のようになっています。

m/hoge/
# 初めのm/の部分は'perlMatch'と'perlMatchStartEnd'というsyntax
# hogeの部分は    'perlMatch'というsyntax
# 最後の/の部分は 'perlMatchStartEnd'というsyntax

上記内容は:help synstackに記載がある通り以下のようにして調べることができます。

for id in synstack(line("."), col("."))
   echo synIDattr(id, "name")
endfor

私は本plugin作成のためにvimrcに以下のように書いてコマンド化してます。

function s:EchoSynName()
  let synlist = []
  for id in synstack(line("."), col("."))
    call add(synlist, synIDattr(id, "name"))
  endfor
  echo synlist
endfunction

command! -nargs=0 EchoSynName call s:EchoSynName()

マッチ演算子の構文を見て分かる通り、囲みの初めと終わりが異なっています。
本pluginでは、この囲みの初めと終わりを、filetypeごとに変数として定義しておき、定義された構文で囲まれている部分を探してtext objectとしています。

filetypeごとの変数を外部変数として定義しているため、カスタマイズ可能です。
ただし、上記の通り、囲まれるsyntaxの初めと終わりのsyntaxは異なる必要があり、さらに囲まれる中身のsyntaxも異なる必要があります。

対応できない例を挙げると、例えばC言語Javaの文字列("hoge")は文字列内のsyntaxが全て同じsyntaxのため、本pluginでは対応できません。

外部変数は以下の用な感じで書きます。

" perlの正規表現だけ対応
let g:enclosedsyntax_custom_mapping = {
  \ 'perl': [
  \   { 'start': ['perlMatch','perlMatchStartEnd'], 'end': ['perlMatchStartEnd'] },
  \]
  \}

g:enclosedsyntax_custom_mapping自体はfiletypeをキーとした辞書であり、filetypeのキーの値として、囲まれるsyntaxをstartとendをキーとして指定した辞書の配列を指定します。

エラーチェックなど入れておらず、対応出来るかどうかも構文シンタックスファイル次第なので、ちょっと難易度が高いかもしれませんがもし気になる方がいれば試してみてください。
そして、バグを見つけたら教えて下さい:p
エラーチェックはそのうち入れます。ドキュメントもそのうち書きたい。。。

pluginの制限(仕様)

カスタマイズしようと考えなければあまり関係ないかもしれませんが、以下の制限(仕様)があります。

  • 対応できる囲みは構文ハイライトファイルに依存する
  • 囲み部分が同一のsyntaxだと対応できない(どちらが囲みのはじめかわからないため)
  • 2点間の囲みだけ。Perlのs演算子(s/hoge/hige/)のような3点間は未対応。

まとめ

まだ、PerlRubyにしか対応していませんが、良かったら使ってみてください。
text objectって便利ですよね。text object(とoperator)を使っているとVimを使っているという気分になるので好きです。皆さんもtext objectをうまく使いこなして快適なVim Lifeを送りましょう。
他にも便利なtextobj-user pluginはたくさんあるので色々試してみてください。text objectって何という方は:help text-objectsや、Webなどで色々調べてみてください。

明日のVim Advent Calendar 2012は、@dice_zuさんです。