Swiper(Ivy系補完)でmigemoする
やはりmigemo便利。
環境: GNU Emacs 27.1, Ivy 0.13.0, Swiper 0.13.0
はじめに
Swiperでmigemoする先行事例として上の記事がある。avy-migemoパッケージ1を使わずに、dashとsの関数を使ってre-builder関数を独自に実装している。
この記事の関数ytn-ivy-migemo-re-builderは現在でもしっかり動く。しかし、「dashとsを使っていて読みにくい」、「ivy-subexps変数を考慮していないためミニバッファでのハイライトが変」、といった問題(個人の感想です)があるので、ivy--regex-plus関数を参考にした関数my/ivy--regex-migemo-plusを作ってみた。
my/ivy--regex-migemo-plus関数の実装
コード全体を最初に載せる (Gist)。
Swiperで使うために作った関数だが、Ivy系の補完ならどれでも使える。
swiper(もしくはswiper-isearch)で使うなら次を評価する:
(setf (alist-get 'swiper ivy-re-builders-alist) #'my/ivy--regex-migemo-plus) ;; (setf (alist-get 'swiper-isearch ivy-re-builders-alist) #'my/ivy--regex-migemo-plus)
使わないようにするなら次を評価する:
(setf (alist-get 'swiper ivy-re-builders-alist nil 'remove) nil) ;; (setf (alist-get 'swiper-isearch ivy-re-builders-alist nil 'remove) nil)
my/ivy--regex-migemo関数
my/ivy--regex-migemo関数はivy--regex関数を参考にして作った関数である。ivy--regex関数の実装を見てもらえれば、してることがほとんど同じだということがわかると思う2。
ivy--regexは文字列を受け取り、正規表現文字列を返す関数である。文字列がスペースを含む場合、スペースで分割してそれぞれグループ化し、間に.*?を挿入した正規表現文字列を返す:
ELISP> (ivy--regex "hello") "hello" ELISP> (ivy--regex "hello world") "\\(hello\\).*?\\(world\\)"
ivy--regexの実装をよく見ると、ivy--split関数が1個要素のリストを返すかそれ以外かで分岐している。ivy--splitは受け取った文字列を分割して文字列のリストを返す関数である:
ELISP> (ivy--split "hello") ("hello") ELISP> (ivy--split "hello world") ("hello" "world")
ivy--split関数が1個要素のリストを返す場合ivy--subexps変数に0をセットし、それ以外の場合ivy--subexps変数にivy--splitが返したリストの要素数をセットしている。
なんだか、ivy--splitで返ってきた文字列のリストの要素それぞれにmigemo-get-pettern関数3をかける処理を挟めば良さそうな感じがしてきたと思う。
my/migemo-get-pattern-shyly関数
が、そのままmigemo-get-pettern関数をかけて返ってきた値を使ってはいけない。migemo-get-petternではほとんどグループ化が使われた正規表現文字列が返ってくる:
ELISP> (migemo-get-pattern "nihonn") "\\(‖\\|ニ\\s-*ホ\\s-*ン\\|ニ\\s-*ホ\\s-*ン\\|日\\s-*本\\|二\\s-*本\\|に\\s-*ほ\\s-*ん\\|n\\s-*i\\s-*h\\s-*o\\s-*n\\s-*n\\|n\\s-*i\\s-*h\\s-*o\\s-*n\\s-*n\\)"
これをそのまま使ってしまうとivy--subexpsで指定したグループ数と合わなくなりハイライトがおかしくなってしまう。
ということで、返ってきた正規表現文字列のグループをshyなグループ4にするmy/migemo-get-pattern-shyly関数を定義した5。この関数を使えば、通常のグループをshyなグループにしたものが返ってくる:
ELISP> (my/migemo-get-pattern-shyly "nihonn") "\\(?:‖\\|ニ\\s-*ホ\\s-*ン\\|ニ\\s-*ホ\\s-*ン\\|日\\s-*本\\|二\\s-*本\\|に\\s-*ほ\\s-*ん\\|n\\s-*i\\s-*h\\s-*o\\s-*n\\s-*n\\|n\\s-*i\\s-*h\\s-*o\\s-*n\\s-*n\\)"
my/ivy--regex-migemo-pattern関数
ここまでくれば、ivy--splitで返ってきた文字列のリストの要素それぞれにmy/migemo-get-pettern-shyly関数をかけておしまいそうだが、あと一つ考えなくてはならないことがある。
Swiper(Ivy系補完)は文字列を正規表現にして補完するだけでなく、そもそも正規表現を受け付けるのだ。
ivy--splitは賢く、単純にスペースで区切ってるのではなく、正規表現の[ ... ]はひとまとまりにして返す:
ELISP> (ivy--split "nihonn[^ ] no") ("nihonn[^ ]" "no") ELISP> (ivy--regex "nihonn[^ ] no") "\\(nihonn[^ ]\\).*?\\(no\\)"
この例でいうと、"nihonn[^ ]"という文字列をmy/migemo-get-pattern-shylyにかけてしまうと、"[^ ]"部分は意味がなくなってしまう。なので、my/migemo-get-pattern-shylyにかけるのは"nihonn"だけにしたい。
ivy--splitの正規表現付きの際の(非自明な)挙動は以下のようになっている:
;; 連続していても[ ... ]の後ろは別要素になる ELISP> (ivy--split "hell[^ ]world") ("hell[^ ]" "world") ;; 連続していてもグループは1要素になる ELISP> (ivy--split "\\(hello\\)\\(world\\)") ("\\(hello\\)" "\\(world\\)")
これを利用して、ivy--splitの結果に対して、
[ ... ]がある場合、[ ... ]以外をmy/migemo-get-pattern-shylyにかける- グループの場合、
my/migemo-get-pattern-shylyせずそのまま - それ以外は
my/migemo-get-pattern-shylyにかける
という挙動のmy/ivy--regex-migemo-pattern関数を定義した。以下で期待どおりに動作していることがわかる:
ELISP> (my/ivy--regex-migemo-pattern "nihonn[^ ]") "\\(?:‖\\|ニ\\s-*ホ\\s-*ン\\|ニ\\s-*ホ\\s-*ン\\|日\\s-*本\\|二\\s-*本\\|に\\s-*ほ\\s-*ん\\|n\\s-*i\\s-*h\\s-*o\\s-*n\\s-*n\\|n\\s-*i\\s-*h\\s-*o\\s-*n\\s-*n\\)[^ ]" ELISP> (my/ivy--regex-migemo-pattern "\\(var\\|custom\\)") "\\(var\\|custom\\)"
これは完璧ではなく、\s-といった正規表現は諦めてmy/migemo-get-pattern-shylyにかけてしまっている。気に入らなかったら各自調整してほしい。
my/ivy--regex-migemo-plus関数
ivy--regexのmigemo版とも言えるmy/ivy--regex-migemoの実装を見てきた。Ivyはivy--regexに少し機能を追加したivy--regex-plus関数をデフォルトで使用する。ivy--regex-plusは文字列に!が含まれているか判定した後に結局ivy--regexを呼ぶ。
my/ivy--regex-migemo-plus関数はivy--regex-plus関数のmy/ivy--regex-migemo版だ。一昔前のfletのようにcl-letfを使っている。
単にivy--regexの呼び出しをmy/ivy--regex-migemoの呼び出しに変えるだけなので、!以降の文字列はmigemo化されない。これも気に入らなければ各自調整してほしい。
結局、my/ivy--regex-migemo-plusは以下のような動作をする:
ELISP> (my/ivy--regex-migemo-plus "nihonn ! hate bashing") (("\\(?:‖\\|ニ\\s-*ホ\\s-*ン\\|ニ\\s-*ホ\\s-*ン\\|日\\s-*本\\|二\\s-*本\\|に\\s-*ほ\\s-*ん\\|n\\s-*i\\s-*h\\s-*o\\s-*n\\s-*n\\|n\\s-*i\\s-*h\\s-*o\\s-*n\\s-*n\\)" . t) ("hate") ("bashing"))
おしまい
avy-migemoが動かなくなってからmigemoを使ってなかったけど、使ってみるとmigemoはかなり便利なことがわかる。「日本語入力オン→ひらがな入力→かな漢字変換→日本語入力オフ」という手順をすっとばせるのはとても強力。
今回、Ivyのソースコードを眺めて色々知らなかったことを学べた。「スペース1個は".*?"になるが、スペース2連続ならスペース1個にマッチする」とか「ivy--regex-plusの!機能」とか。
マニュアルを読んだらどちらもちゃんと書かれていた。私は雰囲気でIvyを使っていました😑
おしまい。