なんだこれは!
これはLisp Advent Calendar 2022およびClojure Advent Calendar 2022の6日目の記事です。
いまさらのvim-sexp入門
この記事は、vim/neovimを使ってLisp一族(Common Lisp, Clojure, Scheme, etc...)に食わせるコードを書いてやろうという人が最初につまづく、vim-sexpの入門記事です。
いまさらですが、誰も日本語の解説書いてないようなので、書きます。
最初にまとめ
- とりあえず各自で利用しているvimプラグインマネージャー経由で以下を入れる。自分はPlugを使ってるのでPlug準拠。ここでは
, {'for': ['clojure', 'scheme']}
をつけているがこの辺は自由にしよう。なくても多分問題ない
Plug 'guns/vim-sexp', {'for': ['clojure', 'scheme']}
Plug 'tpope/vim-sexp-mappings-for-regular-people', {'for': ['clojure', 'scheme']}
Plug 'tpope/vim-repeat', {'for': ['clojure', 'scheme']}
Plug 'tpope/vim-surround'
Plug 'luochen1990/rainbow'
" 他、各自の使うLisp方言に対応するfiletypeとsyntaxのやつ(標準vim付属であるなら不要)
- 英語だが、誰かが書いたチートシートが https://gist.github.com/avescodes/c724b1e0cd651d2ea69a39e4c4437ed8 にある。解説とか不要ならこれだけ読めば完了!でもそんな簡単じゃないからこの記事がある…
前置き:そもそもなんでvimなのか
基本的にssh越しにテキストエディタを使う事を日常的にしたい/せざるをえない人種がいる。
それらの人は当然VSCode等という選択肢はなく大体vimかemacsかの二択になるが、emacsのデフォルトキーバインドでは『alt(meta)キーが押せる』事が前提とされており、altキーが押せないssh越しの場合は『毎回escキーを先押しする』事になりそんなのやってられない訳でemacsは選択肢から外れた。emacsでもevilやspacemacs等のvim型キーバインドはあるようだが他にも色々面倒があり挫折した。
だから我々のような者にはvimしかないのだ。
ただ、今回扱うvim-sexpも元はといえばemacs用のparedit.elというプラグインをvim用に真似したみたいなものなので、emacsには敬意を払おう。自分には使えないが。
でたらめをやってごらん
上に書いたプラグインは導入したか?導入したら、早速適当なLisp系コードをvimで開こう。
今回はvim-sexpの練習なので、事前にコピーしたファイルを開いたり、編集した後にすぐに u
で元に戻したりするとよい。
とりあえず始める前に上記のプラグインについて簡単に説明しておく。
- guns/vim-sexp: 本体。これによって「S式データ構造に沿った移動/編集」ができるようになる。
-
tpope/vim-sexp-mappings-for-regular-people: vim-sexpの標準のキーバインドがあまりにも超越人向けすぎるので、一般人にも比較的理解しやすいキーバインドにしてくれる。この記事でもこの
for-regular-people
のキーバインドで説明を書いている。 -
tpope/vim-repeat: vim-sexpによって追加された編集機能はvimの
.
でリピートしてくれない。これはそれをちょっとだけリピートしてくれる(しかしちょっとだけだ!不完全な場合もあるので、そこは使い方を工夫せざるをえなくなる…)。 -
tpope/vim-surround: 囲み括弧文字の種類を変更できる。Lisp処理系の内の歴史の長い系列のものでは括弧は
()
だけなので不要な事が多いが、例えばClojureでは括弧の種類が()
[]
{}
#{}
とあり、これらの種類を後から変えたいケースがたまにある。種類を変更するにはカーソルを該当位置まで移動させてからcs(変更前の括弧種類の閉じ側)(変更後の括弧種類の閉じ側)
の4キー入力を行う。このcsとは change surround との事。 - luochen1990/rainbow: 括弧の色を入れ子ごとに変更してくれるやつだよ
カーソル移動
何もかもに疲れ果てたvim使いは ctrl-F
とか 9j
とか 99w
とかを雑に使って移動するだろう。もし目的とする文字や語が分かっているのであれば f
や /
が使えるだろう。しかし『Lisp系言語のソースコードは、S式データ構造』であるので、この『S式データ構造』に沿った移動ができるのならそれに越した事はない。vim-sexpがそれを実現する。
vim使いは w
や b
や e
で単語単位で移動する。だからvim-sexpでは W
B
E
によって「今いるリスト内要素単位」で移動できる。もちろん数字prefixでの回数指定もできる。だがこの機能がマッピングされた事により、標準のvimでできた W
B
E
での元々の移動挙動は使えなくなっている。備えよう。
また W
B
E
以外に、 (
で「今いるリスト領域の開始括弧」へと、そして )
で「今いるリスト領域の終了括弧」へと直接ジャンプする事ができる。これは標準のvimの %
に似ている挙動だが、どちらに飛ぶかがより直感的かつ直接指定する事ができる。 %
同様、この機能を使えば「ある特定のブロック範囲を v
で選択して、よそへ持っていく」みたいな事が簡単にできる。後述する強力な編集操作に慣れない内はこれを使って編集していくとよいだろう。
これらの移動は ()
の通常括弧の他、Clojureで採用されている []
や {}
にも対応している。Clojureの #{}
へは一部対応が不完全なケースもあるが大体対応しているし不完全なケースでも {}
と同じ判定にはなっているので大きな問題はない。
[[
と ]]
は、標準のvimではセクション単位の移動だ。だからvim-sexpではこれらはトップレベルS式単位の移動になっている。LispソースコードではトップレベルS式は大体関数やら何やらの定義になっているのでそこそこ使い勝手がよいだろう。ただR6RS Schemeの library
形式とは相性がよくない…。
編集その1
移動は理解した。ではちょっとS式を書いてみよう。空いたトップレベルの適当な場所で i
を押して挿入モードに入り (
を入力してみる。するとすぐうしろに自動的に )
が出現した。これは [
や {
でも同じだ。
これ自体は普通にありがちな補完機能なのだが、ここから通常のvimから乖離が激しくなる。
ctrl-H
やBSキーや、通常モードの x
等によってこれらの括弧の対応を普通に壊す事ができるが、壊した括弧を補給しようとして再度括弧を出そうとすると、邪魔な対まで出てきてしまったり、あるいは閉じ括弧を出す代わりに別の閉じ括弧の位置まで勝手に移動させられたりしてしまう。これは慣れるまではとてもうっとおしい。
とりあえず、なるべく括弧の対応を壊さないような編集をする事に慣れていこう。
(その為には、前述したブロック選択を活用したり、後述の構造編集を行ったりする事になる)
S式のデータ構造は行指向ではないので、行指向でのvim編集に慣れていた人ほど普段のvim手癖が通用しない。マイナスに飛び込む覚悟をしよう。
インデント整形
==
は標準のvimでは「今いる行(もしくは選択中の行全部)をインデントし直す」機能だった。そして前述の通りS式のデータ構造は行指向ではないのでこの ==
も「今いるリスト範囲全体をインデントし直す」機能になっている。今いる行のみならず関連する周辺行もまとめてインデントしてくれるより便利な機能に進化したという理解でokだ。
しかし大体インデントし直したい時というのは上記した周辺行のみならず、今いるトップレベル定義全体をインデントし直したいというケースがそこそこ多い。その為の機能が =-
として定義してある。大体こっちを使えばよいのだが「とても重い」というデメリットもある。大きな関数でこれを押せば何秒も待たされる事だろう。
なおLisp系言語では、通常の値/関数と、スペシャルフォーム/マクロとで、それぞれでインデントルールを変える風習のものが多い。このどちらかにすべきかを制御するのは lispwords および g:clojure_fuzzy_indent_patterns になっている。詳細はリンク先を参照だが、大体、そのLisp系言語を使い込んでいくほどこの定義がvimrcやらinit.vimやらに増えていく事になると思う。
編集その2
既存のソースコードもしくは何らかのS式データを編集する際には「ある特定の引数にだけ別の関数を適用したい」というケースがたまにある。自然言語で書くと分かりづらすぎるのでコード例で示す。
(foo "aaaa" "bbbb" "cccc")
↓これを、楽にこう変更したい
(foo "aaaa" (upper-case "bbbb") "cccc")
通常のvimであれば、「まず "bbbb"
の直前までカーソルを持ってきてから、先に (upper-case
を書き、それから E
あたりで"bbbb"
の末尾までカーソルを移動し、残りの )
を書く」という手順(もしくはこの逆の順序の手順)になるのだが、これはつまり編集の途中段階で括弧の対応を壊すような編集になっていると言える。そして括弧の対応を壊すような編集に対してvim-sexpはとても厳しい。なので、括弧の対応を維持したままこの変更を行いたい。それを楽に行えるキーバインドが提供されている。
"bbbb"
のあたりにカーソルを移動させてから素早く \w
と入力する。すると "bbbb"
が ( "bbbb")
となり、 (
の直後にカーソルが移動し、挿入モードになる。これで後はそのまま残りの upper-case
を入力すれば完了だ。
この機能はバリエーションが多く、 ()
か []
か {}
か、それからカーソルを先頭に持ってくるか末尾に持ってくるか、で6パターンある。チートシートから引用すると、
,w - Surround element with (), place cursor at front
,W - Surround element with (), place cursor at end
,e[ - Surround element with [], place cursor at front
,e] - Surround element with [], place cursor at back
,e{ - Surround element with {}, place cursor at front
,e} - Surround element with {}, place cursor at back
となっている。チートシートでは最初のLeader文字を標準の \
ではなく ,
に変更しているのでこの表記だが、変更していなければ \
だ。自分は変更せずに \
のまま使っている。またこのLeader文字の後の入力は1秒以内とかに素早く入力しなくてはならない。この辺りはvim標準の話になり、また自分もあまり詳しくないので各自で「vim Leader」「vim timeout」あたりで調べてほしい。
そして注意したいのだが、この機能を .
で再実行した際には「括弧の追加」はリピートしてくれるのだが「その後で入力した文字列」はリピートしてくれない!これはvim-repeat側の不具合もしくはvim-sexpとvim-repeatの連携の不具合のようだ。どうしてもこの処理を繰り返したい場合はまず「括弧の追加」だけをしまくってから、それから追加しまくった括弧のところで「文字列の追加」をしまくる2フェーズに処理を分けるとよいだろう。とは言え大体最初の1個を入力した後にこの事に気付くのだが…。
ここで紹介した機能はソースコードを書く際にはあまり使わないが、S式でhtml構造のようなデータを書いたりする際には多用する機能なので、そういう「データ記述」をする事が多ければマスターしておきたい。
データ構造の編集
データ構造を変更する機能は結構多いが、よく使うものだけチートシートから紹介する。
>e - swap an element right
<e - swap an element left
>) - Slurp right (+ (+ 1 2) 2) -> (+ (+ 1 2 2))
<) - Barf right (+ (+ 1 2) 2) -> (+ (+ 1) 2 2)
>( - Barf left (+ (+ 1 2) 2) -> (+ + (1 2) 2)
<( - Slurp left (+ (+ 1 2) 2) -> ((+ + 1 2) 2)
最初の二つは、要素の位置を交換するものだ。「交換」というとあまり使い道がなさそうなイメージだが、要はこれを繰り返す事で、ある要素をひたすら前に持っていったり、後ろへ持っていったりする事ができる。リスト編集の基本操作だ。各Lisp処理系によって名前の違う progn
やら do
やら begin
やらで順番に実行する式ブロック内の一連の式の並びの実行順を入れ替えたりするのにも使える。
後の四つは、リストの拡張もしくは縮小の動作になる。チートシートに書かれている通りの動作をする。
この辺りは「ソースコードを編集する」よりもずっと「記述されたデータ構造を編集する」方になるので、これもS式でhtml構造を表現していてそれを編集する、みたいな状況向きの操作と言える。ただソースコードでもたまに間違っていた括弧対応を直したり、引数構造を直したりで使う場面はある。
おしまい
以上!
vim-sexpが提供している機能とキーバインドはもっとたくさんあるがこれだけ使えたら大体問題ないだろう。だが慣れてきたらチートシートに記載されている他の操作にも挑戦してみよう。今回はelementをターゲットとする操作を主に紹介したが、formをターゲットとするキーバインドの方もマスターするとvimキー操作を更に何手か短縮できるだろう。
これで「S式構造をぐねぐねいじる」感覚を享受していこう。
(追記)この後は、Clojure Advent Calendar 2022の9日目のuochanさんの記事も見て、dps-parinferの導入も検討しよう。ただこれはまたvim-sexpとは別種の新しいルールを学ぶ必要があるので、同時に入れてしまうのではなく、vim-sexpを一通り理解してからの方がいいと思う(一緒に始めてしまうとかなり混乱すると思う)。