つーさにブログ

つうさにのメモ用ブログ

Swiper(Ivy系補完)でmigemoする

やはりmigemo便利。

環境: GNU Emacs 27.1, Ivy 0.13.0, Swiper 0.13.0

はじめに

www.yewton.net

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)。

gist.github.com

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--regexmigemo版とも言える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を使っていました😑

おしまい。


  1. 現在メンテナンスされていない。

  2. my/ivy--regex-migemo関数ではIvyでキャッシュのような役割をしているivy--regex-hash変数は使わないようにしている。

  3. 文字列からmigemoを通した正規表現文字列を返す関数。詳細は先のブログ記事。

  4. \(?: で始まるグループ。match-stringなどでsubexpsとして計上されない。

  5. こういう用途の関数は公式で提供されてそうな気がするけど見つけられなかった。知ってる人いたら教えてください。