Emacsのprescient.elを魔改造してみた

# 背景

こんばんは。Emacsを触り始めて３～４年たった初心者です。
今回、Emacsの[prescient.el](https://github.com/radian-software/prescient.el)パッケージに`tiebreak`という機能が追加されたのを機に魔改造(?)してみました。今回はその手順を紹介します。

## [prescient.el](https://github.com/radian-software/prescient.el)とは

そもそも[prescient.el](https://github.com/radian-software/prescient.el)とは、元々[ivy](https://github.com/abo-abo/swiper)や[selectrum](https://github.com/radian-software/selectrum)、[company](https://github.com/company-mode/company-mode)の補完候補を、`履歴`・`頻度`・`候補の長さ`に並び替えるパッケージで、[selectrum](https://github.com/radian-software/selectrum)のみフィルター機能を提供していました。
この[selectrum](https://github.com/radian-software/selectrum)を皮切りに[vertico](https://github.com/minad/vertico)のようなEmacs標準機能を上手く活用する流れが生まれ、[orderless](https://github.com/oantolin/orderless)、[consult](https://github.com/minad/consult)、[embark](https://github.com/oantolin/embark)等の現在を代表するパッケージも誕生していきました。

[prescient.el](https://github.com/radian-software/prescient.el)もこの流れを汲んで、Emacs標準機能のcompletion-stylesに対応するようになりましたが、フィルター機能を提供する[orderless](https://github.com/oantolin/orderless)がとても柔軟性が高く、動作も早いためほとんどの方が[orderless](https://github.com/oantolin/orderless)を使用しているように感じます。

筆者もフィルター機能は[orderless](https://github.com/oantolin/orderless)、並び替えは[prescient.el](https://github.com/radian-software/prescient.el)というように使い分けをしていましたが、最近`tiebreak`という機能が追加され、長さの並び替えを自由に変更することができるようになりました。

今回は、[prescient.el](https://github.com/radian-software/prescient.el)をmigemo対応させ、候補の長さを[flx](https://github.com/lewang/flx)のスコアリングに置き換える方法を紹介します。

# migemo

`prescient-regexp-regexp`と[こちら](https://nyoho.jp/diary/?date=20210615)の記事を参考に下記のように実装します。

```elisp
(use-package prescient
  :config
  (setq completion-styles '(prescient)
        completion-category-defaults nil
        completion-category-overrides nil)

  (with-eval-after-load 'migemo
    (defun my/prescient-regexp-regexp-advice (orig-fun query &rest args)
      "Advice function to replace QUERY with (migemo-get-pattern query)."
      (let ((migemo-query (migemo-get-pattern query)))
        (apply orig-fun migemo-query args)))
    (advice-add 'prescient-regexp-regexp :around #'my/prescient-regexp-regexp-advice))

  (prescient-persist-mode +1))
```

verticoでprescientを有効にします。

```elisp
(use-package vertico-prescient
  :after vertico
  :config
  (setq vertico-prescient-enable-filtering nil)
  (vertico-prescient-mode +1))
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696232/3ce7fe3f-04b0-53c3-7aa7-c99273d88b57.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696232/08c2929b-8aa4-ccb7-3ab9-1d87cada7ab7.png)

ローマ字で日本語が絞り込まれていることが確認できます。

# tiebreakerの設定

tiebreakerの設定は下記を参考にスコアリングを行うように修正しました。
https://github.com/radian-software/prescient.el/issues/128#issuecomment-1694358951

~~[fuz-bin](https://github.com/jcs-elpa/fuz-bin)を利用した設定は下記の通りです。~~
[flx](https://github.com/lewang/flx)にしました。
lisp初心者なのでもっと良い書き方があれば教えてください…

```elisp
(use-package flx
  :config
  (with-eval-after-load 'prescient
    ;; 入力文字を抽出
    (defvar-local my/input-query nil)
    (defun my/store-input-query (string &rest _args)
      "store the current completion query in `my/input-query'."
      (setq my/input-query (replace-regexp-in-string " " "" string)))
    (advice-add 'completion-all-completions :before #'my/store-input-query)

    ;; ローカル変数を使用できるように再定義
    (defvar vertico--total nil)
    (defvar corfu--total nil)

    (defun my/flx-tiebreaker (c1 c2)
      (if (and (and (< vertico--total 3000)
                    (< corfu--total 3000))
               (> (length my/input-query) 0)
               (< (length c1) 100)
               (< (length c2) 100))
          (let ((query my/input-query))
            (let ((score1 (car (flx-score c1 query flx-file-cache)))
                  (score2 (car (flx-score c2 query flx-file-cache))))
              (if (and (integerp score1) (integerp score2))
                  (cond ((> score1 score2) -1)
                        ((< score1 score2) 1)
                        (t (- (length c1) (length c2))))
                0)))
        (- (length c1) (length c2))))

    (setq prescient-tiebreaker #'my/flx-tiebreaker)))
```

corfu-prescientを適用します。

```elisp
(use-package corfu-prescient
  :after corfu
  :hook (corfu-mode . (lambda () (setq-local prescient-filter-method '(fuzzy))))
  :config
  (setq corfu-prescient-enable-filtering nil)
  (corfu-prescient-mode +1))
```

適用前（長さによる並び替え）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696232/105182a0-f0d2-19df-e5b7-8212bde1d10c.png)

適用後（flxのスコアリングによる並び替え）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696232/ff45f8d3-577f-e6ec-f61d-4dfec84c620d.png)

# 【応用】orderlessとprescientを組み合わせる場合

prescient.elでも十分ですが、
orderlessはより柔軟に補完候補を絞り込むことができるため、
組み合わせた例も紹介します。

## orderlessの設定

orderlessをcompletion-stylesに適用し、
corfuの場合[orderless-fast-dispatch](https://github.com/minad/corfu#auto-completion)・orderless-flexを適用するようにします。

```elisp
(use-package orderless
  :config
  (setq completion-styles '(orderless basic)
        completion-category-defaults nil
        completion-category-overrides nil)

  (with-eval-after-load 'migemo
    ;; orderlessをmigemo対応
    (defun orderless-migemo (component)
      (let ((pattern (downcase (migemo-get-pattern component))))
        (condition-case nil
            (progn (string-match-p pattern "") pattern)
          (invalid-regexp nil))))
    (add-to-list 'orderless-matching-styles 'orderless-migemo))

  (with-eval-after-load 'corfu
    (defun orderless-fast-dispatch (word index total)
      (and (= index 0) (= total 1) (length< word 3)
           `(orderless-regexp . ,(concat "^" (regexp-quote word)))))

    (orderless-define-completion-style orderless-fast
      (orderless-style-dispatchers '(orderless-fast-dispatch))
      (orderless-matching-styles '(orderless-flex)))

    (add-hook 'corfu-mode-hook
              (lambda ()
                (setq-local corfu-auto-delay 0
                            corfu-auto-prefix 1
                            compleion-styles '(orderless-fast basic))))))
```

## flxの設定

```elisp
(use-package flx
  :config
  (with-eval-after-load 'prescient
    ;; 入力文字を抽出
    (defvar-local my/input-query nil)
    (defun my/store-input-query (string &rest _args)
      "store the current completion query in `my/input-query'."
      (setq my/input-query (replace-regexp-in-string " " "" string)))
    (advice-add 'completion-all-completions :before #'my/store-input-query)

    ;; ローカル変数を使用できるように再定義
    (defvar vertico--total nil)
    (defvar corfu--total nil)

    (defun my/flx-tiebreaker (c1 c2)
      (if (and (and (< vertico--total 3000)
                    (< corfu--total 3000))
               (> (length my/input-query) 0)
               (< (length c1) 100)
               (< (length c2) 100))
          (let ((query my/input-query))
            (let ((score1 (car (flx-score c1 query flx-file-cache)))
                  (score2 (car (flx-score c2 query flx-file-cache))))
              (if (and (integerp score1) (integerp score2))
                  (cond ((> score1 score2) -1)
                        ((< score1 score2) 1)
                        (t (- (length c1) (length c2))))
                0)))
        (- (length c1) (length c2))))

    (setq prescient-tiebreaker #'my/flx-tiebreaker)))
```

## prescient.elの設定

prescientも設定します。

```elisp
(use-package prescient
  :config
  (setq prescient-aggressive-file-save t)
  (prescient-persist-mode +1))

(use-package vertico-prescient
  :after vertico
  :config
  (setq vertico-prescient-enable-filtering nil)
  (vertico-prescient-mode +1))

(use-package corfu-prescient
  :after corfu
  :config
  (setq corfu-prescient-enable-filtering nil)
  (corfu-prescient-mode +1))
```

# 終わりに

今回の魔改造(?)はまだまだ完成度としては高くないかもしれませんが、
それでも十分利便性が向上し、とても使いやすくなりました。
cache等を勉強して実装できればいいのですが、まだまだlisp力が足りていないので、
もっと精進していきたいです。