インフィニットループ 技術ブログ

2011年11月28日 (月)

著者 : emonkak

便利なVimのOperatorをマスターする

こんにちわちわ、10月に新しく入りましたemonkakです。
今回は全国1000万人のVimmerのみなさんに、便利なVimのoperatorについて紹介したいと思います。
opepatorとはお馴染みy(yank)d(delete)などの、範囲を取るコマンドのことです。
operatorはカーソルを移動するweなどのmotionに対して使うか、 Visual mode上で使うことができます。
例えば、行の終わりまでyankする場合はy$v$yと打鍵します。
また、operatorはtext-objectに対して使うことができます。
例えば、yiwと打鍵すると現在のカーソルの下の単語をyankするといった動作になります。
この場合yがoperatorで、iwが範囲を示すtext-objectです。
yyのようにoperatorのマッピングを二回打鍵した場合は、現在の行を表わすtext-objectを渡したことになります。
operatorはtext-objectと組み合わせると非常に便利です。
Visual modeで選択してからopepatorを使うより、text-objectに対して使う方がタイプ数が少なくて済むからです。
これでもうVisual modeは不要ですね。
ここで一度、組込みのoperatorとtext-objectをおさらいしておきましょう。

operator一覧

以下がデフォルトで定義されているoperatorです。
c d yとインデント関連以外はあまり使うことはないかもしれません。
ROT13のoperatorがあるのは面白いです。

マッピング 動作
c 変更する
d 削除する
y yankする
~ 大文字小文字を入れ替える (‘tildeop’がセットされていれば)
g~ 大文字小文字を入れ替える
gu 小文字にする
gU 大文字にする
! 外部プログラムでフィルターする
= インデントを整形する
gq テキストを整形する(幅を80に収めたり)
g? ROT13で暗号化する
> 右にインデントする
< 左にインデントする
zf foldを定義する
g@ ‘operatorfunc’に設定された関数を呼び出す

text-object一覧

text-objectは通常a?i?というルールで定義されています。
aは英語の冠詞aを表わし、範囲をそのまま示します。
iはinnerの略でその範囲の内部を示します。
例えば、ibは括弧の内部を示し、abは括弧自体を含めた内容を示します。

マッピング 範囲
aw iw 単語(iskeywordに依存)
aW iW 単語(空白区切り)
as is
ap ip 段落
a] a[ i] i[ [] 括弧
a) a( ab i) i( ib () 括弧
a> a< i> i< <> 括弧
at it htmlタグ
a} a{ aB i{ i} iB {} 括弧
a" i" ダブルクオート
a' i' シングルクオート
a` i` バッククオート

また、textobj-userを導入すると新しくtext-objectを追加できます。
これでより便利にoperatorを使うことができます。
以下に公開されている有用なtext-objectを列挙しておきます。
使用にはtextobj-userが必要です。

text-object 範囲
textobj-datetime 日付と時刻
textobj-diff Diff hank
textobj-entire バッファ全体
textobj-fold Folding
textobj-function 関数
textobj-indent インデントブロック
textobj-jabraces 日本語の鉤括弧など
textobj-lastpat 最後に検索されたパターン
textobj-syntax Syntax highlightの塊
textobj-comment コメント
textobj-between 任意の文字に囲まれた範囲
textobj-paramater 関数のパラメーター

operator-userで新しくoperatorを追加する

組込みのoperatorは少ないですが、自分で追加することもできます。
operatorを新しく追加するには、operator-userを使います。
こちらも有用なものを列挙しておきます。
使用にはoperator-userが必要です。

text-object 動作
operator-replace レジスタの内容でテキストを置き換える
operator-camelize snake_caseとCamelCaseを相互変換する
operator-sequence 複数のoperatorを連続して使用する
operator-html-escape HTML要素をエスケープする
operator-comment ソースコードをコメント・コメントアウトする
operator-sort テキストをソートする

operator-commentとoperator-sortは私が作ったプラグインです。
後に動作を詳しく説明します。

私が定義しているoperator

operator-comment

これは、ソースコードをコメント・コメントアウトするoperatorです。
.vimrcに定義していましたが、長くなったのでプラグイン化しました。
コメント文字列は'commentstring'を見ているのでカスタマイズしたい場合は適宜設定して下さい。
マッピングはarpeggioを使って、oと任意のキーの同時押しにマッピングしています。
operatorのマッピングは2回繰り返して押すと行に対する動作になるので、シングルストロークのマッピングの方が便利です。
しかし、キーはなかなか空いてないので、同時押しにマッピングできるarpeggioを使っています。

	Arpeggio map oc  <Plug>(operator-comment)
	Arpeggio map od  <Plug>(operator-uncomment)
	

textobj-indentがインストールされていると仮定して、動作は以下のようになります。

Before

operator-comment-before
この状態で、ocを同時押ししてtextobj-indentのマッピングiiを打鍵すると、

After

operator-comment-after
このようにインデントに沿ってコメント化されます。
複数行コメントの構文にも対応していますが、'commentstring'は1つしか設定できないので使い分けることはできません。
また、コメントアウトされた状態で、<operator-uncomment>を実行すると元のコメントアウトされていない状態に戻ります。

operator-sort

これはソートを実行するoperatorです。
こちらもプラグインとして公開しています。
operator-userはコマンドからoperatorを作ることができるので:sortを使って以下のように定義することもできます。

	call operator#user#define_ex_command('sort', 'sort')
	

しかし、行に対してしか動作しないので、文字列に対しても動作するようにプラグインを作りました。
行に対するソートは内部で:sortを呼び出しているので、動作は:sortと同じてす。
マッピングは以下のようにしています。

	nmap [Space]S  <Plug>(operator-sort)$
	nmap [Space]s  <Plug>(operator-sort)
	vmap [Space]s  <Plug>(operator-sort)
	

文字列に対する動作は以下のようになります。
operator-sort
ウィンドウの下の部分がoperatorを実行した結果です。
ここではカーソル位置で[Space]sir,と打鍵しています。
irは[]括弧の内部を示すtext-objectです。
,は区切り文字で、文字列に対して実行する場合は区切りが自明ではないので入力する必要があります。

operator-yank-clipboard

これは単にクリップボードにyankするだけのoperatorです。
noremap "+yで十分だと思われるかもしれませんが、2回繰り返して押した時に正しく動かないので定義しています。

	function! OperatorYankClipboard(motion_wiseness)
	  let visual_commnad =
	  \ operator#user#visual_command_from_wise_name(a:motion_wiseness)
	  execute 'normal!' '`['.visual_commnad.'`]"+y'
	endfunction
	call operator#user#define('yank-clipboard', 'OperatorYankClipboard')
	Arpeggio map oy  <Plug>(operator-yank-clipboard)
	Arpeggio map oy  <Plug>(operator-yank-clipboard)
	

operator-translate

これはGoogle翻訳を使って英日翻訳をするoperatorです。
動作にはwebapi-vimが必要になります。
単にechoするだけのシンプルなものですが、ちょっとした英文を訳したい時に便利です。

	function! OperatorTranslate(motion_wiseness)
	  let visual_commnad =
	  \ operator#user#visual_command_from_wise_name(a:motion_wiseness)
	  let query = join(s:get_region("'[", "']", visual_commnad), "\n")
	  let api = 'http://translate.google.com/translate_a/t'
	  let response = http#get(api, {
	  \   'client': 'o',
	  \   'hl': 'en',
	  \   'sl': 'en',
	  \   'tl': 'ja',
	  \   'text': query
	  \ }, {'User-Agent': 'Mozilla/5.0'})
	  if response.header[0] ==# 'HTTP/1.1 200 OK'
	    let result = json#decode(response.content)
	    echo join(map(result.sentences, 'v:val.trans'))
	  else
	    echoerr response.header[0]
	  end
	endfunction
	call operator#user#define('translate', 'OperatorTranslate')
	Arpeggio map ot  <Plug>(operator-translate)
	

このoperatorはs:get_region()というヘルパー関数を使っています。
このヘルパー関数はgetpos()で取る引数を2つと、visualmode()で得られる値を取ります。
動作としてはgetpos()を2回、それぞれの引数で呼んで、2つの座標間のテキストをgetline()で取得して整形しています。
Visual modeで選択してyankすることでも取得できますが、このヘルパー関数はregisterを汚染しないのとカーソルの移動がないのが利点です。
以下にs:get_region()の定義も載せます。

	function! s:get_region(expr1, expr2, visual_commnad)
	  let [lnum1, col1] = getpos(a:expr1)[1:2]
	  let [lnum2, col2] = getpos(a:expr2)[1:2]
	  let region = getline(lnum1, lnum2)
	  if a:visual_commnad ==# "v"  " char
	    if lnum1 == lnum2  " single line
	      let region[0] = s:strpart(region[-1], col1 - 1, col2 - (col1 - 1))
	    else  " multi line
	      let region[0] = s:strpart(region[0], col1 - 1)
	      let region[-1] = s:strpart(region[-1], 0, col2)
	    endif
	  elseif a:visual_commnad ==# "V"  " line
	    let region += ['']
	  else  " block
	    call map(region, 's:strpart(v:val, col1 - 1, col2 - (col1 - 1))')
	  endif
	  return region
	endfunction
	

また、末尾に日本語が来た時のために以下の関数も必要です。

	function! s:strpart(src, start, ...)
	  let str = strpart(a:src, a:start)
	  if a:0 > 0
	    let i = byteidx(strpart(str, a:1 - 1), 1) - 1
	    return i == -1 ? str : strpart(str, 0, a:1 + i)
	  else
	    return str
	  endif
	endfunction
	

これは渡された範囲の文字列で検索を実行するoperatorです。
これもs:get_region()を使ってます。
カーソル下の単語で検索する*#のoperator版と言えます。
私はVisual modeで使えれば十分なので:vmapのみしています。

	function! OperatorSearch(motion_wiseness)
	  let visual_commnad =
	  \ operator#user#visual_command_from_wise_name(a:motion_wiseness)
	  let search_command = v:searchforward ? 'n' : 'N'
	  let region = join(map(s:get_region("'[", "']", visual_commnad),
	  \                     'escape(v:val, "\\/")'),
	  \                 '\n')
	  let @/ = '\V' . region
	  call histadd('/', '\V' . region)
	  silent execute 'normal!' search_command
	endfunction
	call operator#user#define('search-forward', 'OperatorSearch',
	\                         'let v:searchforward = 1')
	call operator#user#define('search-backward', 'OperatorSearch',
	\                         'let v:searchforward = 0')
	vmap *  <Plug>(operator-search-forward)
	vmap #  <Plug>(operator-search-backward)
	

まとめ

operatorは使いこなすと非常に便利です。
みなさんもoperatorを使いこなして快適なVim lifeを送りましょう。

ブログ記事検索

このブログについて

このブログは、札幌市・仙台市の「株式会社インフィニットループ」が運営する技術ブログです。 お仕事で使えるITネタを社員たちが発信します!