たまには sandwich.vim の話をしたい

すこし前の話になりますが sandwich.vim というプラグインを作りました。有名なプラグイン surround.vim のようなもので、文字列を囲んだり、囲まれた文字列から囲みを消したり、置換したりします。 surround.vim に比べて、よりユーザーによる拡張性に重点を置いていて次のような点が特長です。

  • 囲み文字列のペアを式で指定できる
  • 削除/置換される囲み文字列のペアもユーザーが定義できる (surround.vim は追加するペアのみ)
    • 正規表現を使うこともできる
    • 他のテキストオブジェクトの選択範囲差分でも指定できる
  • ユーザーの定義した文字列に囲まれた領域を選択するテキストオブジェクトが使える

ユーザーが設定を追加する場合は、 g:sandwich#recipes というリストに追加していきます。ゼロから作ることもできますが、ひとまずデフォルトの設定をコピーするとよいでしょう。

let g:sandwich#recipes = deepcopy(g:sandwich#default_recipes)

私自身そこまでいろいろ設定しているわけではないのですが、いくつか紹介します。


\(, \) のペアを使う

Vim の正規表現におけるグループ化には \(, \) および、 \%(, \) を使うので Vim script を書いているときにこれらで囲めたり、囲みを消せたりできるといいな、と思うことがあります。

let g:sandwich#recipes += [
      \   {
      \     'buns': ['\(', '\)'],
      \     'filetype': ['vim'],
      \     'nesting': 1,
      \   },
      \
      \   {
      \     'buns': ['\%(', '\)'],
      \     'filetype': ['vim'],
      \     'nesting': 1,
      \   },
      \ ]

'nesting' キーは囲みがネスト構造をつくる場合は 1 、それ以外は 0 にしてください。例えば (, ) のような括弧はネストをつくるので 1、 ', ' のようなクオーテーションはネストをつくらないので 0 になります。

saiw\( と単語の上で入力するとその単語を \(, \) のペアで囲みます。また、 sd\%( と入力すると \%(, \) の囲みを消します。あるいは sdb という入力は常に一番近い囲みを消すのでこれを使うと便利でしょう。 \) という部分は二つの設定で被ってしまっていますが、 sandwich.vim はリスト後方の設定を優先するので saiw\) と入力すると単語を \%(, \) で囲むでしょう。別の入力を使いたければ 'input' キーを設定してください。

let g:sandwich#recipes += [
      \   {
      \     'buns': ['\%(', '\)'],
      \     'filetype': ['vim'],
      \     'nesting': 1,
      \     'input': ['%'],
      \   },
      \ ]

これで saiw% と入力すると単語を \%(, \) で囲むでしょう。

連続するシングルクオーテーション

Vim script だけではなかったと思うのですが、一部の言語にはシングルクオートに囲まれたリテラル文字列中の連続する二つのシングルクオートを一つのシングルクオートとみなす、というルールがあります。

echo 'Why don''t you try sandwich.vim?'     "--> Why don't you try sandwich.vim?

これは Vim の 'quoteescape' (:help 'quoteescape') オプションでは対応できないので、ちょっと不便ですね。 sandwich.vim で何とかしましょう。

let g:sandwich#recipes += [
      \   {
      \     'buns': ["'", "'"],
      \     'filetype': ['vim'],
      \     'skip_regex_head': ['\%(\%#\zs''\|''\%#\zs\)''\%(''''\)*[^'']'],
      \     'skip_regex_tail': ['[^'']\%(''''\)*\%(\%#\zs''\|''\%#\zs\)'''],
      \     'nesting': 0,
      \     'linewise': 0,
      \   },
      \ ]

これで例えば sd' と入力すると連続するクオーテーションをスキップして端のクオーテーションを消します。また、 sandwich.vim は囲まれた文字列を選択するテキストオブジェクトを提供するので、例えば vis' と入力すると連続するクオーテーションをスキップして文字列リテラルを選択します。

'skip_regex' は特定の正規表現にマッチする位置をスキップする機能で、 'skip_regex_head' および 'skip_regex_tail' はそれぞれ先頭・末尾の囲みを検索するときにのみ使われます。

実は文字列リテラルの端に連続するシングルクオートがあり、カーソルがその上にある場合は上手くいきません…。下の文字列リテラル中の # に示される位置がそれに当たりますが、間の文字列にカーソルがあれば大丈夫です。

"    ###              ###
echo '''string literal'''

関数で囲む

たまに関数名を指定して関数で囲みたいと思うことがあります。つまりこういうことです。

foo  --->  func(foo)

これは sandwich.vim の囲みを式で指定する機能を使うと実現できます。

let g:sandwich#recipes += [
      \   {
      \     'buns': ['FuncName()', '")"'],
      \     'expr': 1,
      \     'cursor': 'inner_tail',
      \     'kind': ['add', 'replace'],
      \     'action': ['add'],
      \     'input': ['f']
      \   },
      \ ]

function! FuncName() abort
  let funcname = input('funcname: ', '')
  if funcname ==# ''
    throw 'OperatorSandwichCancel'
  endif
  return funcname . '('
endfunction

'expr' キーが 0 でない値を持つとき 'buns' の要素はそれぞれ式として評価され、評価値で囲みます。 FuncName() 関数はユーザーに関数名を問い合わせ、空でなければ ( をつけて返します。

sandwich.vim は他のオペレータに倣って、デフォルトでは作用した文字列の先頭にカーソルを置きますが、 'cursor' キーを使うことで終了時のカーソル位置を指定できます。この場合は続けて引数を追加したい場合が多いので作用した文字列の末尾にカーソルを移動させます。

この設定は囲む動作でのみ使うので、 'kind', 'action' キーを使って必要になるシチュエーションを制限しています。

f:id:machakann:20160519180358g:plain

あまりキータイプ数の上では得をしていないようにも思われるかもしれませんが、矩形選択範囲に対して使えたり、ドットリピート可能だったりするので、対象が何か所もある場合に便利です。

関数囲みを消す

関数で囲めるのなら、これを消せてもいいと思うのが人情というものだと思います。関数を表す範囲を認識するのはなかなか複雑な仕事になるので既存のテキストオブジェクトを使いましょう。 sandwich.vim には既存のテキストオブジェクトの範囲差分から削除する文字列を指定する機能があるのでこれを使います。

例えば、HTMLなどのマークアップ言語のタグは Vim 組み込みのテキストオブジェクト it, at の差分で指定できます。 at の範囲から it の範囲を除いた部分がタグですね。 (:help it, :help at, :help tag-blocks)

      <--- it --->
<body>hello world!</body>
<--------- at ---------->

textobj-functioncall は関数囲み全体を、 textobj-parameter はカーソル下の引数を選択するので、カーソル直下の引数を残して関数囲みを消すことができます。

     <->    textobj-parameter
func(arg)
<------->   textobj-functioncall

既存のテキストオブジェクトを使う場合は 'buns' キーの代わりに 'external' キーを使います。 sdf と入力することで関数囲みを消します。

let g:sandwich#recipes += [
      \   {
      \     'external': ["\<Plug>(textobj-parameter-i)", "\<Plug>(textobj-functioncall-a)"],
      \     'noremap': 0,
      \     'kind': ['delete', 'replace', 'query'],
      \     'input': ['f']
      \   },
      \ ]

f:id:machakann:20160519180407g:plain

textobj-parameter の選択範囲はカーソル位置に依存するので、残念ながらビジュアルモードではうまく動きませんが、 itat はカーソル位置に寛容なので意図通りに動きます。また、矩形選択は必ずしも矩形である必要はありません。空行は無視されます。

let g:sandwich#recipes += [
      \   {
      \     'external': ['it', 'at'],
      \     'noremap': 1,
      \     'kind': ['delete', 'replace', 'query'],
      \     'input': ['t']
      \   },
      \ ]

f:id:machakann:20160519180411g:plain


複雑そうに見えますが、ひとまず 'buns' キーを設定するだけでも大体いい感じに動くと思います。他にも issue #1 #2 は sandwich.vim にどんなことができるかを端的に説明していて、どうやら Tex など書く場合に便利らしいです。よろしければ sandwich.vim を試してみてください。


おまけ: ハイライト機能

私は画面がぴかぴか光ればキャッキャッと喜ぶおさるさん並みの感性の持ち主なので、 sandwich.vim はこれでもか、というくらい画面がぴかぴかします。囲みを追加した場合には追加した文字列をハイライトするのですが、以前はユーザーが何かキーを押し次第ハイライトを消していたのをしばらく残るように最近書き換えました。 call operator#sandwich#set('all', 'all', 'hi_duration', 1000) と、極端に持続時間を長くしてみるとちょっと楽しいです。ハイライトが遅れて消えます。

f:id:machakann:20160519180416g:plain

なお、ユーザーが何らかの編集を開始した場合はハイライトを直ちに消します。ハイライトを消すのに timer 機能を使っているので新しめの Vim でのみこのように動きます、 timer 機能がない場合は以前と同じように動作します。正直なところ、新しい機能を使ってみたかっただけです。ハイライトなどいらぬ、という人間様は vimrc に次の先進的一行を堂々と刻んでください。

call operator#sandwich#set('all', 'all', 'highlight', 0)

columnmove.vim を書き直した

Vim 組み込みの f t F T ; , w b e ge W B E gE コマンドを縦 (列) 方向に模倣したモーションを提供するプラグインです。長いことバグとか放置していたのを何とかしました。

columnmove.vim

columnmove-f

f t F T コマンドは特定の文字が出てくるところまでカーソルを移動するコマンドです。そのままだととんでもねぇ眼力を要求されるので、移動先をハイライトするようになっています。

columnmove-f

そんな軟弱なものはいらん、という硬派な方、眼力MAXな方、 set cursorcolumn 運用の方は次の行を vimrc に追加してください。

let g:columnmove_highlight = 0

また、移動先ハイライト中 (ユーザーの入力待ち中) には以下のキーでスクロールできます。

上へスクロール 下へスクロール
一行ずつ <C-y> <C-e>
半ページずつ('scroll' オプションに依存) <C-u> <C-d>
一ページずつ <C-b> <C-f>

columnmove-w

w b e ge コマンドは単語の境界まで移動するコマンドです。列方向なので「単語」という言い方は適切ではないかもしれませんが。書いてから、欲しかったやつとは微妙に違うことに気が付いてやっつけで機能を追加した経緯があります。

let g:columnmove_strict_wbege = 0

このように vimrc に書いておくと文字があるかないかの境界に沿ってカーソルを移動します。つまり iskeyword オプションを考慮しませんし、スペースだろうがタブ文字だろうが文字があればカーソルを止める点が通常の w 等のコマンドとは違ってきます。例えば次のような列を考えてみましょう、ハイフンはスペースだと思ってください。

a
b

"
c
d

-
e

一行目の a から、 通常であれば w コマンドが止まるのは ", c, e です。しかし上記の設定をしていた場合、止まるのは ", - になります。ちなみに W コマンドであれば ", e ですね。

キーマッピングについて

私は gvim を使うことが多いので、デフォルトだと <M-f> <M-w> などのメタキー (Altキー) を使ったキーシークエンスへマッピングされます。ターミナルをお使いの方の場合これらのキーシークエンスは有効でないかもしれないので、お手数ですがマッピングしなおしてお使いください。 columnmove#utility#map() 関数が便利です。

" デフォルトのキーマッピングはいらない
let g:columnmove_no_default_key_mappings = 1

" columnmove#utility#map({モード}, {コマンドの種類}, {キーシークエンス})
" モード : n -> ノーマルモード
"         x -> ビジュアルモード
"         o -> オペレータ待機モード
"         i -> インサートモード
" 種類 : `f` `t` `F` `T` `;` `,` `w` `b` `e` `ge` `W` `B` `E` `gE` のどれか
" キーシークエンス : `:map` コマンドの `{lhs}` と同じように解釈される。
call columnmove#utility#map('nxo', 'f', '\f')
call columnmove#utility#map('nxo', 'w', '\w')

必要なものだけで十分だと思いますが、次のようにすると、まとめてマップできて便利です。

let g:columnmove_no_default_key_mappings = 1
for s:x in split('ftFT;,wbeWBE', '\zs') + ['ge', 'gE']
    call columnmove#utility#map('nxo', s:x, '\' . s:x)
endfor
unlet s:x

ユーザーさんに教えてもらいました。\ 始まりのキーシークエンスへマップされます。

オペレータと組み合わせて使う場合

もともと、 j k コマンドの延長のように考えていたので、デフォルトだとオペレータ待機モードで行指向に働きます。つまりカーソルの通る行全体が処理の対象です。

call columnmove#utility#map('nxo', 'e', '\e')

" Press \e to delete all.
abc
def     ->
ghi

ただし、columnmove#utility#map() 関数の第四引数に 'block' を与えていると矩形指向で働きます。

call columnmove#utility#map('nxo', 'w', '\w', 'block')

" Press \e to delete block-wise.
abc            bc
def     ->     ef
ghi            hi

こっちの方が便利な場合も多いかもしれませんね。どちらの場合でも o_v o_V o_CTRL-V は有効 (参考 :help o_v :help o_V :help o_CTRL-V) なのでどちらにしてもさほど困らないと思います。 o_v とか自体あまり使う人いない気もしますが。ビジュアルモード使えばいいですし。


もともと、 Vim script の練習のつもりで書き始めた初めてのプラグインなのですが、思いのほか長いこと使ってます。実際のところ、すでに世の中にはもっと万能で汎用的なモーションコマンドがたくさん発明されているので、それらに比べると、とりわけ便利というものでもないです。ただ、単純なのでマクロ (:help complex-repeat) のなかでよく使ったりしてます。地味に便利。

緑っぽい Vim のカラースキームを書いた

青っぽい色が好きなので大体青い感じのカラースキームを好んで使っていたんですけど、 Vim を使っているんだし緑もいいかな、って思って書きました。

vim-colorscheme-tatami

set background=light はかなり明るく、set background=dark もそんなに暗くないです。書き始めた時に畳っぽく見えたので名付けたんですけど、以来ずっと「でも畳の緑色って結構すぐ抜けるよな…」と思い続けています。まあ、いいでしょう。

六畳間

六畳間

なんか四畳半はできないらしいです。

tyru.hatenablog.com

tyru.hatenablog.com

tyru.hatenablog.com

vim-vimhelplint を書きました

ドキュメントは大事です。自分で書いたプログラムですら、しばらくすると使い方を忘れてしまいます。おおよそ、自分で書いてきた vim プラグインにはドキュメント書いてきたのですが、あとあとになってミスを見つけたりしてつまらないコミットを増やすこともしばしばです。と、いうわけで vim のヘルプ用にタグなどをチェックしてくれるツールを書きました。

vim-vimhelplint

使い方は簡単で、 vim のヘルプファイルの編集を開始し、

:VimhelpLint

するだけです。疑わしいところがあれば quickfix へ登録されます。 :copen するなどしましょう。

:VimhelpLint!

すると自動で quickfix window を開きます。

精度はまだまだだと思いますので、必ず自分の目と手で確認してください。なお、このツールにヘルプファイルはありません。いろいろあって追加されました!

swap.vim についてあれこれ

先日、 swap.vim というものを書きました。コンマ区切りの要素の順番を簡単に入れ替えるためのプラグインです。

今のところ、デフォルトではコンマで区切られたテキストだけに反応します。しかし、ユーザーが設定することでコンマ区切り以外のテキストも扱えます。詳しくは help に書いてありますが、よくわからなければ issues で聞いてください。区切り文字にはゼロ幅マッチも使えるので、例えばキャメルケース(あるいはパスカルケース)の文字列の部分を入れ替えたりできます。

let g:swap#rules  = deepcopy(g:swap#default_rules)
let g:swap#rules += [
      \   {'mode': 'n', 'body': '\<\h\w*\>', 'delimiter': ['\C\ze[A-Z]']},
      \ ]

demo1

この設定自体はそんなに便利でもないですね。他にも、行選択あるいは矩形選択と組み合わせて使うと行単位で入れ替えたりできます。これには特に設定は必要ありません。 demo2

以前は編集範囲が広いと動作が遅くなっていたのが、最近のコミットでずいぶん改善しました。また、何か思いつけばよくしていこうと思います。

swap.vim を書きました

思い返してみると、実際たいしたことはないうえに頻繁にあるわけでもないんですけど、関数の引数だったり配列の要素だったりの順番を入れ替えたくなることがあります。

call func(arg1, arg2, arg3)

これが例えば arg2arg3 を入れ替えるとかだったら問題ないんです。 , arg2 を delete して、

call func(arg1, arg3)

arg3 の後ろへ paste 、

call func(arg1, arg3, arg2)

できましたね。世の中には vim-textobj-parameter あるいは fork版 という便利なものがあるので、これらを使えばいっそう心の平穏が保たれます。ところが arg1arg3 を入れ替えるとなると、うってかわって心穏やかではいられません。 arg1 を切り取ったり、コンマを移動させたり面倒なわけです。この辺 Vim がうまいことやってくれるといいなー、とは思っていました。

ところが、上の例くらい簡単ならいいんですけど、関数が引数に関数を取ったり、

call func(arg1, func(arg1, arg2, arg3), arg3)

任意の文字列を含んだり、

call func(arg1, 'foo, bar')

してくると、なかなか複雑です。しかも、言語によって違う部分もあるので難しい。そんなわけでしばらく放置していたんですけど、まあそこそこいい感じに動く、ぐらいなら可能かもしれないと思いたって swap.vim を書きました。

このプラグインは三つのキーマッピング g<, g>, gs を定義します。g< はカーソル下のアイテムを直前のアイテムと入れ替えます。 g> はカーソル下のアイテムを直後のアイテムと入れ替えます。最初の例

call func(arg1, arg2, arg3)

でいえば、カーソルを arg2 に置いて g< を入力すると arg2arg1 が入れ替わり、

call func(arg2, arg1, arg3)

一方、 g> を入力すると arg2arg3 が入れ替わります、

call func(arg1, arg3, arg2)

gs はちょっと変わり種で、 swap mode と呼んでいるモードのようなものを開始して、インタラクティブに要素入れ替えを行います。 swap mode では h, l によって隣り合う要素の入れ替え、 j, k によるカーソルの移動、 u, <C-r> による簡単なアンドゥ・リドゥ、数字 1 から 9 による入れ替えアイテムの直接指定ができます。納得のいく順番になったらご存知のように <Esc> でノーマルモードに戻ります。

これらはドットリピートで繰り返すことができますが、注意する点が一つあります。 g<, g> は常にカーソル下のアイテムとの相対位置で働く一方で、 gs が繰り返される場合は何番目のアイテムか、という絶対位置で働きます。この違いを覚えておかないと予期しない結果を得るでしょう。

一応、非明示的な行継続がある場合でもいい感じにできるようにインデントをスキップするようにしています。また、設定次第では行継続文字を無視もできます。例えば Vim script であればデフォルトで設定されているので行頭の \ をスキップします。

demo

大きなブロック (移動しているもの) があるのは {} によるグループ化を考慮しているためです。

julialang v0.4

先週 julia の最新安定バージョンの 0.4 がリリースされたみたいですね。早速ビルドしてみました。Windows を使っているので msys2 を使ってここにあるとおりにビルドしてます。ちょっと前まで master をビルドしようとすると gmp のビルドでこけてたんですけど なんかいつの間にか治ってました。 issue 閉じなくていいのかな。

ビルドはなぜか openblas が cpu の自動認識に失敗したので OPENBLAS_TARGET_ARCH を直接指定してもう一度 make 走らせたらいけました。

make -j 4 OPENBLAS_TARGET_ARCH=HASWELL

こんな感じ。OPENBLAS_TARGET_ARCH に指定できる物は deps/openblas/TargetList.txt に列挙されてます。不思議なのですが openblas 単体でビルドしようとすると問題なく cpu 認識するようです。何が違うんだろうか。

もう一つ、なぜか usr/lib/sys.ji が生成されませんでした。多分これが原因だと思うんですけど、うちの環境だと起動に 5s 前後かかってしまう…。 0.3.11 のときはビルド時に自動的に生成されていたと思うんですけど。地味につらいのでいろいろ考えた結果、よく見ると usr/lib/sys.dll は生成されているようなので、起動時に --precompiled=yes つけるようにすると <1s で起動するようになりました。とりあえず常に --precompiled=yes つけるバッチファイル書いてパス通ったところにおいて満足しています。

@echo off
path\to\julia --precompiled=yes %*

実行オプションに --output-ji とか system image 生成するためのものがあるっぽいんですけどうまくいかない…。

ERROR: could not open file boot.jl

とか言われる。

まだちょっと触っただけですけど、 Incremental code caching とかいう新機能のおかげで package の読み込みがかなり早くなっているのに驚きました。初回使用時にコンパイルのために待つことになるので、いっそのこと Pkg.add() のときにやってくれてもいいかなー、と思いました。コンパイルされたパッケージのイメージファイル (多分、~/.julia/lib/ 以下のファイル) が生成されてしまえば以降は速いですね。多分パッケージをアップデートしたらまた、コンパイルしなおす感じでしょう。一番最初に julia をさわったときは起動の遅さとパッケージ読み込みの遅さ(Gadflyとか)が引っかかっていたので、今回ので解消されてしまいました。いくつかコンパイルできなかったパッケージもあったみたいなので、もしかしたら将来もう少し早くなるのかも。

もともと数値計算を主眼に置いているので、息をするように複素数を扱えて、瞬きをするように行列計算できるうえ、多重ループ書いても速いとか夢みたいな言語なのでひそかに応援してます。