世の流れ的には、HelmといえばKubernetes用のパッケージマネージャー を思い浮かべる人が多いと思いますが、私にとってはHelmといえばやはりemacsのパッケージ を思い浮かべます。emacs-helm(以下helm)は所謂fuzzy finder的なモノの一種で、emacsを使っていない人は雑にfzfとかpecoみたいな絞り込みをemacsでできるようにするやつ、と思えば想像しやすいと思います。

helmはなかなか歴史の長いパッケージで、2012年5月15日にanything.elからForkをしたというコミット が打たれています。私個人としては2015年7月にhelmを導入したっぽい記録(commit /tweet )が残っています。そこから数日前まで、かれこれ8年余りの期間にわたってhelmを使ってきました。使ってきた、とは言っても導入したときに多少使い勝手の調整をして以来、ほとんど設定は変えておらず、機能的にもhelm-M-xhelm-find-fileshelm-show-kill-ringhelm-buffers-listくらいしか使っていませんでした。基本的にはMELPAからインストールされる最新版を使っていたのですが、8年使っていてもアップデートで壊れた記憶は多分1回くらいしかなくて、かなりの頻度でコミットが打たれているのに安定していた、という印象があります。

8年の間にも、Ivy/Counsel が話題になったり、icomplete-vertical-mode/fido-vertical-modeという補完UIが標準に追加されたりと、いろいろと流行が変わっていたのは認識はしていたのですが、まぁなんやかんやあり8年間helmを使い続けてきました。

そうこうしていたところ、最近emacs 29.1がリリースされ、同じく長らく使用してきたuse-package (どのタイミングで導入したのかは不明ですが、GitHub上でのfirst commit で既に導入済みだったっぽいのでhelmより長く使っていることになります)がemacs標準搭載となりました。use-package以外のパッケージに関する設定はuse-packageを使ってやっていて、他のパッケージがインストールされていないときにインストールするというのもuse-packageでやっていて、という感じなのですが、そうなるとuse-packageは誰が入れてくれるんだ、ということになり結局use-packageだけはpackage-install直接呼び出すことでインストールしていた のですが、use-packageがbuilt-inとなったことで、晴れてpackage-installの直接呼び出しも不要となりました 。その変更ついでに、いろいろと設定を見直したりとか、ローカルでのみ設定されていた諸諸をGitHubにpushしたりとか、そんなことをやったのですが、その一環として(?)helmからvertico に乗り換えてみました。

helmからverticoに移行して一番の大きな違いは、helmはそれ自体が巨大パッケージで、いろいろなモノがhelmでまかなわれていましたが、verticoはそれ自体はかなり小さく保たれていて、いろいろなパッケージを組み合わせることで同じような機能を実現する、というところです。個人的にはでかいフレームワークより小さいライブラリ類を組み合わせてアプリを書く方が好きなので、そういった意味でもverticoは好きになることができそうです。

今のところ組み合わせているものはmarginaliaorderless の二つ。

marginaliaはminibuffer completionに何やら情報を足すやつでM-xでコマンドの簡単な説明が見られたり、find-fileでファイルのパーミッションやら最終更新時間やらが見られるようになります。インストールしただけでも情報が見れるようになって便利なのですが、そのままだとファイルの更新時間が2023 Sep 10みたいなかんじで月名を使った表示になっており、分からなくはないんですけど日本人としてはやはり2023-09-10と数字を使って表現してくれた方がぱっと見でわかりやすいので、次の様にadviceでmarginalia--time-absoluteを置き換えることで数字表記に修正しました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
(defun marginalia--time-absolute--month-number (time)
    "Format TIME as an absolute age but use month number instead of month name."
    (let ((system-time-locale "C"))
      (format-time-string
       (if (> (decoded-time-year (decode-time (current-time)))
              (decoded-time-year (decode-time time)))
           " %Y-%m-%d"
         "%m-%d %H:%M")
       time)))
(advice-add 'marginalia--time-absolute :override #'marginalia--time-absolute--month-number)

orderlessは候補の絞り込みをするときに、完全一致や前方一致だけでなく、fuzzy matchをしてくれるようになるやつで、たとえばabcと入力するとa.*b.*c.*にマッチする奴を返してくれるようになります(多分これは触らないと便利さはわかりにくい・・・)。

また、helmの時にどうなっていたかはもう既に記憶にない(いつも手癖で操作しているので・・・)んですけど、magitを使用していて、feature branchを切ってgit push -uするときに候補の順番に違和感があったので次の様にmagit-completing-read-functionを調整しました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
(defun sort-preferred-remote-first (branches)
  (let ((preferred-push-remote-prefix "origin/"))
    (nconc (seq-filter (lambda (x) (string-prefix-p preferred-push-remote-prefix x)) branches)
           (seq-remove (lambda (x) (string-prefix-p preferred-push-remote-prefix x)) branches))))

(defun magit-completion-table-with-sort (collection)
  (lambda (string pred action)
    (if (eq action 'metadata)
        '(metadata (display-sort-function . sort-preferred-remote-first))
      (complete-with-action action collection string pred))))

(defun builtin-completing-read
    (prompt choices &optional predicate require-match initial-input hist def)
  "Wrapper for standard `completing-read' function to be used by magit."
  (pcase this-command
    ('magit-push-current-to-upstream
     ;; use my sort function to sort candidate branches
     (setq choices (magit-completion-table-with-sort choices))
     ;; I don't want origin/master to be on top when push
     ;; ref. https://github.com/magit/magit/blob/7bef529ce9b07808d5c14692c5ab2d248e7b5bd1/lisp/magit-push.el#L141
     (when (equal def "origin/master") (setq def nil)))
    (_
     ;; otherwise use magit's default completion-table function
     (setq choices (magit--completion-table choices))))
  (completing-read prompt choices predicate require-match initial-input hist def))

(setq magit-completing-read-function #'builtin-completing-read))

ざっくりと説明をすると、

  • upstream remoteにpushをすることはほぼないのでoriginのブランチが先に来るようにソート
  • origin/masterにpushすることもほぼないので、completing-readに与えるdefaultをnilに上書き という設定です。あんまりemacs lispに詳しくないので、magit-completion-table-with-sortcollectionvoidだというエラーが出て解消に数日かかりました。。。(lexical-binding: tが必要) ここ数日このへんを深夜にいじっていて、emacs/emacs lispへの理解がグッと深まった感じがします。adviceとかも書けるようになりました。

加えて、nerd-icons-completion も導入したので見た目もちょっと可愛い感じになりました。

find-file with marginaria, nerd-icons-completion

まだもうちょっといじる予定ではありますが、とりあえずvertico (and friends)は使い続ける見込みです。