Emacsの次世代ミニバッファ補完UI
2021-06-11T11:47:45+0900
先日、ネイティブコンパイルEmacsが登場でElispをネイティブコードにコンパイルすることによりEmacsの高速化が実現されたという記事を書きましたが、Emacsに到来している新しい波はこれだけではありません。Emacsを華麗に操作するユーザーインターフェイス(以下、UI)にも新潮流がきています。
百聞は一見にしかずというわけで、まずはこちらの動画をご覧ください。
この動画ではEmacsの起動時点から次の操作を行なってファイルを開いています。
利用している外部コマンドはさておき、標準のEmacsと大きく違うところは、コマンドを実行するとミニバッファが縦に伸びて補完候補が表示され、キー入力による候補の絞り込みをしてファイルを開いているところになります。
Anything/Helmと違ってウィンドウ分割を使わず、Emacs標準のミニバッファをうまく拡張して作られたこのUIは、Sublime Textが生んでAtomやVSCodeなどの近代エディタでも採用されているコマンドパレットとも違う、Emacsらしい高い拡張性を実現しています。
この記事では、この新潮流の中心となるパッケージの紹介と、その機能について紹介していきたいと思います。
Emacsの補完UIパッケージ変遷 #
MELPAメンテナでおなじみのPurcellさんはEmacsにきているこの新しい流れをこのようにツイートしています。
The new wave of Emacs minibuffer libraries (selectrum/consult/prescient/embark) provides a *really* nice user experience. Just switched my config from ivy+counsel to these, very happy. 😻
— Steve Purcell (@sanityinc) January 15, 2021
「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*
バッファが開いて補完候補が表示されます。
同様にC-x b(M-x switch-to-buffer
)を使ってバッファを切り替えるときもTABを押せば*Completion*
バッファでバッファ一覧を確認できるようになっています。
*Completion*
バッファに表示されている補完候補は、クリックしたりカーソルをのせてRETで選択できるのですが、マウスは使いたくないし、ミニバッファにフォーカスがあるとき、いちいちバッファを切り替えるのも面倒なので、*Completion*
バッファから選択するという操作は使われていません。
Verticoの補完UI #
次はVerticoの補完UIを見てみましょう。先ほどの最初と同じくM-xからdescribe-
と入力してみます。
すると今度はM-xを押した瞬間、*Completion*
バッファのかわりにミニバッファが縦に広がりコマンドの入力履歴が表示され、文字入力をするとTABを押さなくても補完候補がミニバッファに絞り込まれるようになりました。
そして、ミニバッファでは↑、↓、C-p、C-nで候補を選んで、そのままRETで選択できるため、キーボード操作のみで直感的に候補選択できました。
このように新潮流のミニバッファ補完UIでは、インクリメンタル(いまだとリアルタイムとか非同期とか言ったほうが一般的かもしれませんね)に補完候補の絞り込みを行うAnythingの文化を引き継ぎつつ、よりシンプルで拡張性の高いUIを提供してくれるようになっています。
モダンミニバッファ補完環境の構築 #
新しい補完UIの概要を説明したところで、実際に僕が行ったモダンなミニバッファ補完環境の構築を紹介していきたいと思います。
導入パッケージ #
今回、僕が試してみたパッケージは次のとおりで、minadさんとoantolinさんという2人の開発者のパッケージになっています。
- minad/vertico: vertico.el - VERTical Interactive COmpletion
- ミニバッファ補完UIのみを提供する
- minad/consult: consult.el - Consulting completing-read
- 補完候補リストの作成と便利な補完コマンドを提供する
- minad/marginalia: marginalia.el - Marginalia in the minibuffer
- 補完項目にMarginalia(傍注:要するに追加情報)を表示する機能のみを提供する
- oantolin/orderless: Emacs completion style that matches multiple regexps in any order
- 順不同のスペース区切り補完スタイル(completing style)のみを提供する
- oantolin/embark: Emacs Mini-Buffer Actions Rooted in Keymaps
- キーボード操作のコンテキストメニューのみを提供する
このパッケージの組み合わせで、バッファやファイルを開く便利コマンドM-x consult-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
するようにと言ったほうが分かりやすいかも)検索して、マッチする行を抽出してくれるコマンドです。
通常のgrep
やM-x occur
と違うところは、ミニバッファに検索文字列を入力すれば非同期でマッチした行が絞り込まれてくため、非常に軽快にバッファ内の検索が行える点です。
また、ミニバッファで補完候補を上下に移動すれば、(color-moccurみたいに)即座に該当する行へジャンプしてくれるようになっています。
もちろん、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-outline
はoutline-regexp
変数の値を利用して現在バッファのアウトライン一覧を作成し、そこから検索や移動が行えるコマンドです。
markdown-modeなどで見出し一覧を表示して、それを選択して移動したりすれば大変便利に使えることでしょう。
起動するとすべての見出しが表示され、ミニバッファに文字列を入力すれば絞り込みが行えるため、見出しが多くてもすぐに目的の見出しを見つけられます。
M-x consult-goto-line
: 行番号に移動する
#
M-x consult-goto-line
はM-x goto-line
と同じく入力した行番号へと移動するコマンドです。
M-x 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-grep
、M-x consult-ripgrep
: grep
やripgrep
を使って検索して移動する
#
M-x consult-grep
とM-x consult-ripgrep
はその名のとおり、grep
toripgrep
(rg
コマンド)を使って検索するコマンドです。検索対象となるのは現在バッファだけでなく、現在バッファが存在するディレクトリのファイルと、そのサブディレクトリのファイルも対象となります。
M-x consult-ripgrep
は内部でM-x consult-grep
と同じ関数を利用しているので、実行するコマンドがgrep
とrg
ということ以外は基本的に同じ動作になっています。
M-x consult-grep
とM-x consult-ripgrep
は非同期で検索を行うため、通常のM-x grep
やM-x ripgrep-regexp
に比べて検索ワードが有効かどうかがすぐにわかります。補完候補を移動すればプレビューもされるので、効率よく検索が行えます。
M-x consult-grep
もM-x consult-ripgrep
もスペース区切り検索には対応していないため、複数の検索ワードを使った絞り込みには対応していませんが、もしスペース区切りで検索してみたいと思ったらCunsultの作者が作っているaffe.el - Asynchronous Fuzzy Finder for Emacsというパッケージがあり、こちらも後ほど紹介します。
M-x consult-buffer
、M-x consult-buffer-other-window
、M-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
はバッファ一覧、ファイル一覧、ブックマーク一覧など、複数の補完候補をひとつにまとめて串刺しで絞り込めるグルーピングに対応しています。そのため、とりあえず起動して何か文字を入力すればだいたい目的のバッファおよびファイルに移動できます。
補完候補のグループは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-find
はfind
コマンドを使って現在バッファのディレクトリ(正確にはdefault-directory
の値)、もしくはconsult-project-root-function
が設定されていればプロジェクトルートからファイル検索して、補完候補一覧に表示します。
もし、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 ‘<キーワード>’」と表示されます。このとき、キー入力を行えばアクションが実行できますが、最初はどのキーにアクションがマッピングされているのか分からないと思います。
そこで、C-hを押すと、Verticoを使ってミニバッファにキーマップ一覧が表示されアクションの確認、絞り込み、そして実行ができます。
global-set-key
というElispのシンボルにカーソルがある状態でM-x embark-act
を実行すると、上図のようなアクションが実行できるようになっています。
それでは、次はConsultからEmbarkを使ってみましょう。連携の基本となるのはM-x embark-export
コマンドです。このコマンドは現在の補完候補からEmbarkバッファを作成するコマンドで、作成されたバッファは設定に応じたメジャーモードになります。
例えば、M-x consult-line
から補完候補を絞り込んだ状態でキーバインドを使ってembark-act
を実行して、embark-export
(E)を実行すると、絞り込まれた候補が*Embark Export Occur*
バッファへと出力されます。そして、このバッファはOccur modeが選択されているので、M-x 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-find
とM-x affe-grep
の2つのコマンドを提供してくれていて、これらのコマンドを使えばM-x consult-find
やM-x consult-grep
でできなかったスペース区切りによる複数のキーワードを使った絞り込みが行えるようになります。
M-x affe-find
はデフォルトでfind
コマンド、M-x affe-grep
はデフォルトでrigprep
コマンドを使うようになっていて、それぞれaffe-find-command
とaffe-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-find
とM-x consult-ghq-grep
の2つになっています。
どちらのコマンドも実行すると、補完候補としてghq list
コマンドから習得したリポジトリ一覧を使います。そして選択したリポジトリにM-x affe-find
かM-x affe-grep
を実行します。
Affeではなく、Consultの方のコマンドを使いたいという人は、consult-ghq-find-function
とconsult-ghq-grep-function
変数から変更も可能です。
まとめ #
この記事ではEmacsの新しいミニバッファ補完UIについて、歴史的な背景から各種パッケージの特徴、具体的な使い方まで紹介しました。
今回紹介したパッケージを実際に使いはじめるまでは、正直Anything/Helmと比べてそこまで大きな違いはないだろうと思っていたのですが、使ってみると非同期で作られた各種機能による操作性がとても快適で、ウィンドウ分割もしないためウィンドウレイアウトを乱される心配もなくなるなど、想像以上に便利に感じました。
また、調べていく中でEmacs 28からはicomplete-vertical-modeというマイナーモードが標準で入ることを知りました。そのため、ミニバッファを縦に伸ばして補完するスタイルは、EmacsのスタンダードなUIになっていく可能性があります。
そういうこともあって、僕もこの流れに乗って完全に移行しようと決意して色々と調べた結果、この記事を書くに至りました。
当初の想定よりもだいぶ長い記事になってしまったのですが、それでも紹介できたのはまだまだ一部というのが正直な感想です。
なので、最後にもっと色々と便利に設定するための参考リンクを紹介して、この記事を締めたいと思います。長い記事でしたが最後までお読みいただきありがとうございました。