EmacsでMarkdownの見出し構造を
別バッファに表示する
(imenu-list)

関連記事

1. Markdownの見出し構造を見たい

Markdownで長い記事を書くとき、文章全体の構造を把握しながら書き進めたいと思うことがあります。

これまで outline-minor-mode を使ってきました。
TABで見出しを折りたたみ、C-c C-n で次の見出しへ移動する使い方です1

ある程度うまく機能していたのですが、文書が長くなると不便な点が出てきました。
折りたたんでいると全体像が見えず、展開すると今どこにいるかがわかりにくい。
「章の並びを確認しながら、今書いている節を編集する」という行き来が増えると、このやり方には限界がありました。

そこで、別バッファに見出し一覧を常時表示する imenu-list を試しました。

1. Markdownの見出し構造を見たい
(use-package imenu-list
  :ensure t
  :bind (("C-c i" . imenu-list-smart-toggle))
  :config
  (setq imenu-list-auto-update t
        imenu-list-auto-resize t
        imenu-list-focus-after-activation t))

(with-eval-after-load 'markdown-mode
  (setq markdown-nested-imenu-heading-index t))Code language: Lisp (lisp)

2. imenu-listとは

imenu-list は、imenuのインデックスを別バッファに一覧表示するパッケージです。

Emacs標準の機能 にimenu があり、バッファ内の関数定義や見出しをインデックス化できます2
imenuは、Markdownの見出しも認識します3

imenu-listを使うと、*Ilist* という名前のバッファに見出し階層が表示され、編集中のバッファの変化に合わせて更新されます。

2.1. インストールと設定

(use-package imenu-list
  :ensure t
  :bind (("C-c i" . imenu-list-smart-toggle))
  :config
  (setq imenu-list-auto-update t
        imenu-list-auto-resize t
        imenu-list-focus-after-activation t))Code language: Lisp (lisp)

imenu-list-auto-update t を設定しておくと、見出しを追加・変更したときに一覧側も追従します。

ただし、完全なリアルタイムではなく、書いた直後に反映されないこともあります。
そのときは M-x imenu-list-update で手動更新します。
よく使うなら g にキーを割り当てておくと楽です4

(with-eval-after-load 'imenu-list
  (define-key imenu-list-major-mode-map (kbd "g") #'imenu-list-update))Code language: Lisp (lisp)

markdown-mode との組み合わせが前提です。
まだ、入れていなければ、こちらも必要です。

(use-package markdown-mode
  :ensure t
  :mode (("\\.md\\'" . markdown-mode)
         ("\\.markdown\\'" . markdown-mode)))Code language: Lisp (lisp)

2.2. 使い方

Markdownバッファで C-c i を押すと、*Ilist* バッファが横に開きます5
もう一度押すと閉じます6

*Ilist* バッファ上では、RET で該当見出しへジャンプ、TAB で階層の展開・折りたたみができます。

見出し構造だけを見るなら、*Ilist* には触れず、編集バッファ側で作業を続けるのが自然な流れです。
章の順序を確認しながら節を書く、あるいは今どのあたりにいるかを確認する、という用途にそのまま使えます。

2.3. 階層が表示されないとき

はじめ、*Ilist* がフラットな一覧になっていて階層が出ませんでした。

2.3. 階層が表示されないとき

そこで、init.elに以下の設定を追加しました。

(with-eval-after-load 'markdown-mode
  (setq markdown-nested-imenu-heading-index t))Code language: Lisp (lisp)

ただ、このままでは、すぐに反映されません。

原因は imenu のインデックスがバッファを開いた時点でキャッシュされるためです。

設定変更後にバッファを開き直さないと反映されません。
インデックスリストは、以下で確認できます。

M-x eval-expression RET
(progn (imenu--make-index-alist t) imenu--index-alist)Code language: Lisp (lisp)

結果がフラットなリストであれば、バッファを M-x revert-buffer で読み直すか、一度閉じて開き直してください。その後 C-c i*Ilist* を開き直すと、### の下にインデントされた状態で表示されます。

3. outline-minor-modeとの比較

outline-minor-mode は「今いる章の中で作業する」のに向いています。

折りたたみで不要な部分を隠し、章単位で移動する編集スタイルです。

imenu-list は「全体を俯瞰しながら、今いる場所を把握する」のに向いています。
一覧を常時表示しておき、必要な章へジャンプする使い方です。

どちらかを選ぶというより、目的が違います。
長い記事を書くときは、imenu-list で全体像を確認しながら、outline-minor-mode のキーバインドで章間を移動する、という組み合わせが実際には使いやすいです7

3.1. そのほかの代替手段(consult-imenu)

常時サイドバーが不要で「必要なときだけ見出し一覧を出したい」なら、consult-imenu が合います。

ミニバッファで候補を絞り込みながらジャンプできます。
Redditでも「imenu-listの代わりにembarkの永続候補リストで代替した」という報告がありますが、常時表示が不要ならこちらのほうが軽い使い心地です。

consult-imenuconsult パッケージに含まれています。
M-g i にバインドしておくのが一般的です8

(use-package consult
  :ensure t
  :bind (("M-g i" . consult-imenu)))Code language: Lisp (lisp)

consult-imenu を呼び出すと、ミニバッファに見出し一覧が展開され、インクリメンタルに絞り込めます。
選択すると該当箇所へジャンプし、キャンセルすると元の位置に戻ります。

用途別にまとめると次のようになります。

常時、別バッファで目次を見たい      → imenu-list
今いる章だけに集中して折りたたみたい  → outline-minor-mode
必要なときだけ見出しへ移動したい     → consult-imenuCode language: PHP (php)

Markdownで文章量が増えてきたと感じたら、imenu-list を試してみる価値はあります。

  1. C-c C-n(次の見出しへ)・C-c C-p(前の見出しへ)・C-c C-u(親見出しへ)は、markdown-mode が outline-minor-mode のコマンドに割り当てたキーバインドです。標準の outline-minor-mode のデフォルトキーとは異なります。 – EmacsでMarkdownのアウトライン編集をする(markdown-mode, outline-minor-mode)
  2. imenu はEmacsに標準搭載されており、追加インストールは不要です。メニューバーへの追加は imenu-add-menubar-index、キーボードからの呼び出しは M-x imenu(Emacs 29以降は M-g i)で行えます。 – Imenu (GNU Emacs Manual)
  3. markdown-mode は imenu に対応しており、# で始まる見出しをインデックス対象として扱います。markdown-nested-imenu-heading-index(デフォルト: t)を有効にすると、見出し階層がネストした形式で表示されます。無効にすると、フラットな一覧になり、キーボード補完での絞り込みが速くなります。 – Markdown Mode and Imenu
  4. g*Ilist* バッファ内の公式キーバインドとしても記載されています。そのほか SPC で「フォーカスを移さずに該当箇所をプレビュー」、q でウィンドウを閉じる操作も使えます。 – imenu-list README
  5. デフォルトの表示位置は右側です。imenu-list-position'left'above'below に変更すると、表示位置を左・上・下に切り替えられます。ウィンドウの幅は imenu-list-size で整数(列数)またはフレームに対する比率(0〜1 の少数)で指定できます。 – bmag/imenu-list
  6. imenu-list-smart-toggle*Ilist* バッファの表示状態を見て開閉を切り替えます。以前は imenu-list-minor-mode を直接バインドする例が多かったのですが、こちらはバッファの可視状態ではなくマイナーモードのフラグのみを参照するため、状態がずれることがあります。公式READMEも smart-toggle の使用を推奨しています。 – bmag/imenu-list
  7. imenu 側のリスキャンを有効にする imenu-auto-rescan t を設定しておくと、見出しの追加・削除が imenu のインデックスにも反映されます。大きなファイルではスキャンに時間がかかる場合があり、imenu-auto-rescan-maxout(バイト数)でサイズの上限を指定できます。 – Imenu (GNU Emacs Manual)
  8. consult は Emacs の標準 completing-read API をベースとしており、Vertico・Mct・デフォルト補完など複数の補完フレームワークと組み合わせて使えます。consult-imenu のほかに、プロジェクト全体の同じメジャーモードのバッファをまたいで検索できる consult-imenu-multi もあります。 – GNU ELPA – consult