Emacsの次世代ミニバッファ補完UI

2021-06-11T11:47:45+0900
emacs

先日、ネイティブコンパイルEmacsが登場でElispをネイティブコードにコンパイルすることによりEmacsの高速化が実現されたという記事を書きましたが、Emacsに到来している新しい波はこれだけではありません。Emacsを華麗に操作するユーザーインターフェイス(以下、UI)にも新潮流がきています。

百聞は一見にしかずというわけで、まずはこちらの動画をご覧ください。

この動画ではEmacsの起動時点から次の操作を行なってファイルを開いています。

  1. ghqでリポジトリ検索 → fdでファイル検索
  2. ghqでリポジトリ検索 → ripgrepでテキスト検索

利用している外部コマンドはさておき、標準のEmacsと大きく違うところは、コマンドを実行するとミニバッファが縦に伸びて補完候補が表示され、キー入力による候補の絞り込みをしてファイルを開いているところになります。

Anything/Helmと違ってウィンドウ分割を使わず、Emacs標準のミニバッファをうまく拡張して作られたこのUIは、Sublime Textが生んでAtomやVSCodeなどの近代エディタでも採用されているコマンドパレットとも違う、Emacsらしい高い拡張性を実現しています。

この記事では、この新潮流の中心となるパッケージの紹介と、その機能について紹介していきたいと思います。

Emacsの補完UIパッケージ変遷 #

MELPAメンテナでおなじみのPurcellさんはEmacsにきているこの新しい流れをこのようにツイートしています。

「Emacsのミニバッファライブラリにおけるニューウェーブは、本当に素晴しいユーザー体験を提供します」と言っており、very happy. 😻で締め括っていることからも、その喜びが伝わってきます。

Emacsは歴史が長いソフトウェアなので、ついつい時の流れを忘れてしまい、「Ivy/Consultが出たのは、つい最近じゃなかったっけ?」とか「いまだにAnything/Helmを使ってるんだけど何が違うの?」などと思ってしまいます。

そこで、まずはEmacsの代表的な補完UIパッケージをおさらいしてみましょう。

パッケージ名 誕生年 解説
Anything 2007 インクリメンタル補完の便利さを広く世に知らしめた記念すべき最初のフレームワーク。ウィンドウを分割して補完候補を表示する。すべてはここから始まった。
Helm 2012 Anythingのフォークとして誕生。多くのプラグインが作られ、保守的なAnythingに対して、破壊的変更を入れてでも成長を優先した。
Swiper 2013 補完候補の表示にミニバッファを活用した最初の補完UIパッケージ。Ivy/Counselのために作られた。
Selectrum 2019 Swiperと似たUIで、Emacs標準の補完APIを利用しつつ、機能拡張を目指した補完UIパッケージ。
Vertico 2021 Emacs標準の補完候補(*Completion*バッファに表示されるアレ)と完全な互換性を持つことを目指して作られた補完UIパッケージ。

すべてのはじまりであるAnythingが生まれてから10年以上、その後にミニバッファ補完UIの方向性を作ったSwiperも8年が経過していて、長い安定と停滞がありました。そんな中、ここ数年で誕生したSelectrumとVerticoというパッケージが登場したことにより、再び活発化してきていることがわかります。

なお、Emacsには他にも多様な補完UIがあります。詳しくはSelectrum in comparison to other completion-systemsにまとめられているので、興味がある人は読んでみてください。

新潮流のミニバッファ補完UIの特徴 #

Emacsは標準でミニバッファ補完UIが備わっていて、Emacsに入門した人は、誰もがとりあえずTABを押して補完する操作を身につけるくらいです。

そんな標準の補完UIと新潮流の補完UIの何が違うのでしょうか。

標準の補完UI #

まずは標準で補完UIについておさらいしておきます。

例えばM-xからdescribe-と入力してTABを押すと、*Completion*バッファが開いて補完候補が表示されます。

M-x describe-で補完しようとしたEmacs

同様にC-x bM-x switch-to-buffer)を使ってバッファを切り替えるときもTABを押せば*Completion*バッファでバッファ一覧を確認できるようになっています。

C-x bで補完しようとしたEmacs

*Completion*バッファに表示されている補完候補は、クリックしたりカーソルをのせてRETで選択できるのですが、マウスは使いたくないし、ミニバッファにフォーカスがあるとき、いちいちバッファを切り替えるのも面倒なので、*Completion*バッファから選択するという操作は使われていません。

Verticoの補完UI #

次はVerticoの補完UIを見てみましょう。先ほどの最初と同じくM-xからdescribe-と入力してみます。

すると今度はM-xを押した瞬間、*Completion*バッファのかわりにミニバッファが縦に広がりコマンドの入力履歴が表示され、文字入力をするとTABを押さなくても補完候補がミニバッファに絞り込まれるようになりました。

VerticoでM-x describeと入力したEmacs

VerticoでM-x describeと入力したEmacs

そして、ミニバッファではC-pC-nで候補を選んで、そのままRETで選択できるため、キーボード操作のみで直感的に候補選択できました。

このように新潮流のミニバッファ補完UIでは、インクリメンタル(いまだとリアルタイムとか非同期とか言ったほうが一般的かもしれませんね)に補完候補の絞り込みを行うAnythingの文化を引き継ぎつつ、よりシンプルで拡張性の高いUIを提供してくれるようになっています。

モダンミニバッファ補完環境の構築 #

新しい補完UIの概要を説明したところで、実際に僕が行ったモダンなミニバッファ補完環境の構築を紹介していきたいと思います。

導入パッケージ #

今回、僕が試してみたパッケージは次のとおりで、minadさんとoantolinさんという2人の開発者のパッケージになっています。

このパッケージの組み合わせで、バッファやファイルを開く便利コマンドM-x consult-bufferを使ったときの画面は次のようになります。

M-x cunsult-bufferの画面

Vertico: ミニバッファ補完UIの提供 #

M-x consult-bufferコマンドを実行するとVerticoのミニバッファ補完UIが画面下で開きます。文字入力可能なミニバッファが一番上にきて、その下に補完候補一覧が1行づつ表示されます。

Consult: 補完コマンドの提供 #

Anything/Helm/Ivyで便利に使っていた補完コマンドはConsultから提供されます。M-x consult-bufferはHelmでいうところのM-x helm-for-filesに該当するコマンドで、現在Emacsで開いているバッファやrecentfの履歴などから補完候補のグループを作成して検索できます。

Orderless: 補完スタイルの提供 #

Emacs標準の補完スタイルは先頭一致になっているため、候補一覧からの絞り込には少し不向きです。そこでOrderlessの補完スタイルを組み合わせます。この画像では、l nと入力して、init.elファイルやmarginalia-20210605.1213ディレクトリなどがヒットしています。このようにOrderlessを使うと、順不動でスペース区切りでいい感じにマッチしてくれる絞り込みに適した補完が行えるようになります。

Marginalia: 項目情報の提供 #

Marginaliaは補完候補一覧の右側に項目情報を表示してくれます。バッファ一覧であればバッファの状態(未保存やRead/Write)、メジャーモード、サイズ、ファイルパスなどが表示され、ファイル一覧であればパーミッション、グループ、オーナー、サイズ、最終更新日などが表示され、Emacsのコマンドであればコマンドの説明などが表示されるなど、項目の属性にあわせた情報を表示してくれます。

Embark: コンテキストメニューの提供 #

画像ではまだ紹介していませんが、Embarkは右クリックメニューのようにカーソル位置から何かしらのアクション(Helmでいうところのhelm-select-action)を実行する機能を提供してくれます。Consultと組み合わせるにはembark-consultパッケージを追加します。後ほど具体的な機能と操作方法を紹介します。

小さな機能のパッケージを組み合せる利点 #

このように、最近のパッケージは目的ごとに機能を分けて開発されているため、とりあえずひとつのパッケージを入れれば使えるというお手軽さはないのですが、目的が制限されているため、他のパッケージと組み合わせて利用できたり、コード量を抑えてメンテナンスしやすくなっています。

必要最小限の設定 #

package.elを利用してMELPAからパッケージをインストールした前提で、上記の画面を再現する必要最小限の設定は次のとおりです。

;; 補完スタイルにorderlessを利用する
(with-eval-after-load 'orderless
  (setq completion-styles '(orderless)))

;; 補完候補を最大20行まで表示する
(setq vertico-count 20)

;; vertico-modeとmarginalia-modeを有効化する
(defun after-init-hook ()
  (vertico-mode)
  (marginalia-mode)
  ;; savehist-modeを使ってVerticoの順番を永続化する
  (savehist-mode))
(add-hook 'after-init-hook #'after-init-hook)

;; embark-consultを読み込む
(with-eval-after-load 'consult
  (with-eval-after-load 'embark
    (require 'embark-consult))

savehist-modeを使うと、Verticoで表示される補完候補の順番が永続化されて便利なため、有効化しておくと良いでしょう。init.elに上記の設定を記述してEmacsを再起動すれば、新しいミニバッファ補完UIのEmacsがはじまります。

Consultの基本操作 #

ここからはConsultが提供するコマンドの基本操作を詳しく紹介していきます。ConsultはAnything/HelmやIvy/Councelが提供していた便利な補完コマンドを提供してくれるパッケージです。

その中から代表的な機能を紹介していきますので、もし良さそうに思えたら、ぜひ試してみてください。

M-x consult-line: 現在バッファを行検索して移動する #

M-x consult-lineは現在バッファをgrepするように(Emacsの場合はM-x occurするようにと言ったほうが分かりやすいかも)検索して、マッチする行を抽出してくれるコマンドです。

通常のgrepM-x occurと違うところは、ミニバッファに検索文字列を入力すれば非同期でマッチした行が絞り込まれてくため、非常に軽快にバッファ内の検索が行える点です。

また、ミニバッファで補完候補を上下に移動すれば、(color-moccurみたいに)即座に該当する行へジャンプしてくれるようになっています。

M-x consult-line

M-x consult-line

もちろん、Orderlessを使うことで複数のキーワードを組み合わせて絞り込みできるようになっています。というか、標準の補完スタイルだと行頭からの一致になってしまい、あまり便利だと感じられないと思うので、この機能を使う場合は補完スタイルを変更することをオススメします。

僕はこのコマンドが気に入ったので、Verticoに慣れるためにもベビーに使ってみようと思い、次のようにして利用しています。

;; C-uを付けるとカーソル位置の文字列を使うmy-consult-lineコマンドを定義する
(defun my-consult-line (&optional at-point)
  "Consult-line uses things-at-point if set C-u prefix."
  (interactive "P")
  (if at-point
      (consult-line (thing-at-point 'symbol))
    (consult-line)))

;; C-s(isearch-forward)をmy-consult-lineコマンドに割り当てる
(global-set-key (kbd "C-s") 'my-consult-line)

;; C-s/C-rで行を移動できるようにする
(with-eval-after-load 'vertico
  (define-key vertico-map (kbd "C-r") 'vertico-previous)
  (define-key vertico-map (kbd "C-s") 'vertico-next))

M-x consult-outline: アウトラインを検索して移動する #

M-x consult-outlineoutline-regexp変数の値を利用して現在バッファのアウトライン一覧を作成し、そこから検索や移動が行えるコマンドです。

markdown-modeなどで見出し一覧を表示して、それを選択して移動したりすれば大変便利に使えることでしょう。

M-x consult-outline

M-x consult-outline

起動するとすべての見出しが表示され、ミニバッファに文字列を入力すれば絞り込みが行えるため、見出しが多くてもすぐに目的の見出しを見つけられます。

M-x consult-goto-line: 行番号に移動する #

M-x consult-goto-lineM-x goto-lineと同じく入力した行番号へと移動するコマンドです。

M-x goto-lineと違って、番号を入力すれば確定しなくてもプレビューで該当行が表示されるため、確定前に何度でも行番号を修正できます。

M-x consult-goto-line

M-x consult-goto-line

もし、このコマンドが気に入ってM-x goto-lineのかわりに常用したいと思うのであれば、次のようにしてM-x goto-lineのキーバインドを差し替えることもできます。

;; goto-lineのキーバインドをconsult-goto-lineにしてしまう
(global-set-key [remap goto-line] 'consult-goto-line)

M-x consult-grepM-x consult-ripgrep: grepripgrepを使って検索して移動する #

M-x consult-grepM-x consult-ripgrepはその名のとおり、greptoripgreprgコマンド)を使って検索するコマンドです。検索対象となるのは現在バッファだけでなく、現在バッファが存在するディレクトリのファイルと、そのサブディレクトリのファイルも対象となります。

M-x consult-ripgrepは内部でM-x consult-grepと同じ関数を利用しているので、実行するコマンドがgreprgということ以外は基本的に同じ動作になっています。

M-x consult-grep、M-x consult-ripgrep

M-x consult-grepM-x consult-ripgrep

M-x consult-grepM-x consult-ripgrepは非同期で検索を行うため、通常のM-x grepM-x ripgrep-regexpに比べて検索ワードが有効かどうかがすぐにわかります。補完候補を移動すればプレビューもされるので、効率よく検索が行えます。

M-x consult-grepM-x consult-ripgrepもスペース区切り検索には対応していないため、複数の検索ワードを使った絞り込みには対応していませんが、もしスペース区切りで検索してみたいと思ったらCunsultの作者が作っているaffe.el - Asynchronous Fuzzy Finder for Emacsというパッケージがあり、こちらも後ほど紹介します。

M-x consult-bufferM-x consult-buffer-other-windowM-x consult-buffer-other-frame: バッファを切り替える #

M-x consult-bufferはすでに説明したとおりM-x helm-for-filesのようにバッファを切り替えるためのコマンドです。現在バッファを切り替えるM-x consult-buffer、他のウィンドウのバッファを切り替えるM-x consult-buffer-other-window、他のフレームのバッファを切り替えるM-x consult-buffer-other-frameの3種類が用意されています。

M-x consult-buffer

M-x consult-buffer

M-x consult-bufferはバッファ一覧、ファイル一覧、ブックマーク一覧など、複数の補完候補をひとつにまとめて串刺しで絞り込めるグルーピングに対応しています。そのため、とりあえず起動して何か文字を入力すればだいたい目的のバッファおよびファイルに移動できます。

補完候補のグループはconsult-buffer-sources変数で変更可能になっており、Anything/Helmで便利だった機能がConsultにも無事に引き継がれていることがわかります。

(defcustom consult-buffer-sources
  '(consult--source-hidden-buffer
    consult--source-buffer
    consult--source-file
    consult--source-bookmark
    consult--source-project-buffer
    consult--source-project-file)
  "Sources used by `consult-buffer'.

See `consult--multi' for a description of the source values."
  :type '(repeat symbol))

また、ミニバッファで接頭辞を付けることで特定の補完候補グループのみを表示できます。例えばf<スペース>だとファイルを対象とする候補、p<スペース>だとプロジェクトの候補、<スペース><スペースだと非表示バッファの候補のみを表示します。この接頭辞はconsult-narrow-helpコマンドからヘルプ表示ができるようになっています。

;; ミニバッファで?を入力して接頭辞のヘルプを表示する
(define-key consult-narrow-map
  (vconcat consult-narrow-key "?") #'consult-narrow-help)

なお、現在バッファのプロジェクトのみを対象とする補完候補を利用するには、consult-project-root-function変数にプロジェクトルートを取得するための関数をセットする必要があります。具体例がConsultの設定例で紹介されています。

;; project.elを使ってプロジェクトルートを取得する関数をセットする
(setq consult-project-root-function
      (lambda ()
        (when-let (project (project-current))
          (car (project-roots project)))))

M-x consult-find: ファイルを検索して移動する #

M-x consult-findfindコマンドを使って現在バッファのディレクトリ(正確にはdefault-directoryの値)、もしくはconsult-project-root-functionが設定されていればプロジェクトルートからファイル検索して、補完候補一覧に表示します。

M-x consult-find

M-x consult-find

もし、findコマンドよりfdを使いたい場合はconsult-find-command変数にコマンドをセットするだけで簡単に切り替えられるようになっています。

;; fdコマンドを使ってconsult-findを利用する
(setq consult-find-command "fd --color=never --full-path ARG OPTS")

もっと便利な活用方法 #

ここまでConsultが提供するコマンドを紹介してきましたが、次はさらに便利な活用方法を紹介していきます。

EmbarkとConsultの連携 #

EmbarkとConsultを連携させるとより高度な操作が行えます。embark-consultパッケージを導入していれば、Consultから作成した補完候補一覧に対してEmbarkのアクションを実行できるようになります。連携方法を紹介する前に、まずはEmbarkの基本操作を簡単に説明しておきます。

EmbarkはM-x embark-actコマンドから、キー操作でアクションを実行するパッケージで、カーソル位置からコンテキストに応じたアクションが選択できます。例えば、文字列であれば文字列に対して、リージョンであればリージョンに対して、そしてConsultコマンド実行中であればConsultに対してアクションが選べます。

Embarkアクションを実行するとき毎回M-x embark-actコマンドを実行するもの面倒なので、何かキーバインドを設定しておくとよいでしょう。

;; Embarkを起動する
;; (global-set-key (kbd "C-S-a") 'embark-act) ; READMEに書かれているキーバインド
(global-set-key (kbd "s-e") 'embark-act) ; 僕が暫定で設定しているキーバインド

Embarkコマンドを実行すると、ミニバッファに「Act on ‘<キーワード>'」と表示されます。このとき、キー入力を行えばアクションが実行できますが、最初はどのキーにアクションがマッピングされているのか分からないと思います。

M-x embark-act

そこで、C-hを押すと、Verticoを使ってミニバッファにキーマップ一覧が表示されアクションの確認、絞り込み、そして実行ができます。

embark-keymap-help

global-set-keyというElispのシンボルにカーソルがある状態でM-x embark-actを実行すると、上図のようなアクションが実行できるようになっています。

それでは、次はConsultからEmbarkを使ってみましょう。連携の基本となるのはM-x embark-exportコマンドです。このコマンドは現在の補完候補からEmbarkバッファを作成するコマンドで、作成されたバッファは設定に応じたメジャーモードになります。

例えば、M-x consult-lineから補完候補を絞り込んだ状態でキーバインドを使ってembark-actを実行して、embark-exportE)を実行すると、絞り込まれた候補が*Embark Export Occur*バッファへと出力されます。そして、このバッファはOccur modeが選択されているので、M-x occur-edit-modeを使った編集ができます。

consult-line → embark-export → occur-edit-mode

consult-line → embark-export → occur-edit-mode

つまり、M-x consult-grepで絞りこんでからwgrepで編集したり、M-x consult-findで絞り込んでからwdiredで編集ということも可能になります。

Affeを使ったファジーマッチング #

Affe(Asynchronous Fuzzy Finder for Emacs)はConsultの作者が作ったFuzzy Finderライクな検索が行えるパッケージです。

このパッケージはM-x affe-findM-x affe-grepの2つのコマンドを提供してくれていて、これらのコマンドを使えばM-x consult-findM-x consult-grepでできなかったスペース区切りによる複数のキーワードを使った絞り込みが行えるようになります。

M-x affe-find、M-x affe-grep

M-x affe-findM-x affe-grep

M-x affe-findはデフォルトでfindコマンド、M-x affe-grepはデフォルトでrigprepコマンドを使うようになっていて、それぞれaffe-find-commandaffe-grep-command変数から利用するコマンドが変更できます。

また、Orderlessと組み合せて利用するための設定も用意されているので、あわせて設定しておくと良いでしょう。

(setq
 ;; Orderlessを利用する
 affe-highlight-function 'orderless-highlight-matches
 affe-regexp-function 'orderless-pattern-compiler
 ;; findのかわりにfdを利用する
 affe-find-command "fd --color=never --full-path")

Affeは、通常のConsultコマンドと同じくEmbarkと組み合わせて利用できます。また、consult-project-root-functionを設定していれば、プロジェクトルートから実行してくれるようになるので、こちらも設定しておくと良いでしょう。

consult-ghqを使ったghq連携 #

最後は最初のデモ動画でも紹介したconsult-ghqパッケージを紹介します。

このパッケージは、現時点ではghqを使ってリポジトリ選択機能を付けたAffeになります。そのため提供するコマンドも、M-x consult-ghq-findM-x consult-ghq-grepの2つになっています。

M-x consult-ghq-find、M-x consult-ghq-grep

M-x consult-ghq-findM-x consult-ghq-grep

どちらのコマンドも実行すると、補完候補としてghq listコマンドから習得したリポジトリ一覧を使います。そして選択したリポジトリにM-x affe-findM-x affe-grepを実行します。

Affeではなく、Consultの方のコマンドを使いたいという人は、consult-ghq-find-functionconsult-ghq-grep-function変数から変更も可能です。

まとめ #

この記事ではEmacsの新しいミニバッファ補完UIについて、歴史的な背景から各種パッケージの特徴、具体的な使い方まで紹介しました。

今回紹介したパッケージを実際に使いはじめるまでは、正直Anything/Helmと比べてそこまで大きな違いはないだろうと思っていたのですが、使ってみると非同期で作られた各種機能による操作性がとても快適で、ウィンドウ分割もしないためウィンドウレイアウトを乱される心配もなくなるなど、想像以上に便利に感じました。

また、調べていく中でEmacs 28からはicomplete-vertical-modeというマイナーモードが標準で入ることを知りました。そのため、ミニバッファを縦に伸ばして補完するスタイルは、EmacsのスタンダードなUIになっていく可能性があります。

そういうこともあって、僕もこの流れに乗って完全に移行しようと決意して色々と調べた結果、この記事を書くに至りました。

当初の想定よりもだいぶ長い記事になってしまったのですが、それでも紹介できたのはまだまだ一部というのが正直な感想です。

なので、最後にもっと色々と便利に設定するための参考リンクを紹介して、この記事を締めたいと思います。長い記事でしたが最後までお読みいただきありがとうございました。