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を使っていました😑
おしまい。