2020-04-15 ,

org-mode文書をタグなどで検索したときにエントリの中身(本文)を全部表示する(Org 9.3.6)

org-modeで書かれた文書を org-match-sparse-tree を使ってをタグで検索する(Sparse Tree を作る)とマッチしたヘッドラインだけが表示されるのですが、そのとき本文も全て一括で表示して欲しかったのでどうしたらよいのか調べてみました。

例えば DIARY タグで検索して日記を通して読みたいときにいちいち一つ一つのヘッドラインをTABキーで展開してまわるのは面倒です。かといってS-TABだとマッチしていない部分の本文も展開されてしまいます。

マニュアルのSparse Treeの項目を見てもマッチしたエントリを移動する操作くらいしか書いていませんでした。

Sparse Trees (The Org Manual)

しかたがないので org-match-sparse-tree 関数(org.el内)を調べてみることにしました。

org-match-sparse-tree の亜種を作る

org-match-sparse-tree は内部で org-scan-tags を呼んでいます。 org-scan-tags はその名の通りの文書をスキャンしてマッチする部分を巡回する関数です。マッチしたところで何をするかは引数 action で指定することになっていて、 org-match-sparse-tree では 'sparse-tree を渡してマッチした場所をハイライトしたりヘッドラインを表示したりしています。 action には任意の関数も渡せるようになっているので、そこにサブツリーを表示する関数を渡してマッチした部分を子孫も含めて全て表示させることにしました。

org-match-sparse-tree のコードをベースに修正してみます。

;; org-match-sparse-treeをベースに修正
(defun org-match-sparse-tree-show-subtree (&optional todo-only match)
  "Create a sparse tree according to tags string MATCH.

MATCH is a string with match syntax.  It can contain a selection
of tags (\"+work+urgent-boss\"), properties (\"LEVEL>3\"), and
TODO keywords (\"TODO=\\\"WAITING\\\"\") or a combination of
those.  See the manual for details.

If optional argument TODO-ONLY is non-nil, only select lines that
are also TODO tasks."
  (interactive "P")
  (org-agenda-prepare-buffers (list (current-buffer)))
  (let ((org--matcher-tags-todo-only todo-only))

    ;; 修正点ここから
    ;; まずはトップレベル以外は全て非表示にする。ハイライトももしあれば解除する。
    (org-overview)
    (org-remove-occur-highlights)
    ;; 修正点ここまで

    (org-scan-tags
     ;; 修正点ここから
     ;; マッチしたところをハイライトしてからサブツリーを表示する
     (lambda ()
       (and org-highlight-sparse-tree-matches
            (org-get-heading) (match-end 0)
            (org-highlight-new-match
             (match-beginning 1) (match-end 1)))

       (org-show-subtree) )
     ;; 修正点ここまで
     (cdr (org-make-tags-matcher match))
     org--matcher-tags-todo-only)))

元々の (org-scan-tags 'sparse-tree ....) では、マッチした場所で (org-show-context 'tags-tree) を呼び出しており、 (org-show-context)(org-show-set-visibility) を呼び出してその場所を表示状態にします。どのような表示を行うかは org-show-context-detail 変数でカスタマイズできるのですがサブツリー全体を表示するというオプションはありません。 'local が一番近いのですが、なぜか次のエントリーのヘッドラインまで表示してしまいます(ドキュメントにもそう書いてあるので意図した動作のようです)。他のオプションには if point is not on headline, also show entry and all children 等と書いてあり、ヘッドライン外にポイントがないと内容は表示してくれません。しかたがないので (org-show-context) の代わりに直接 (org-show-subtree) を呼ぶようにしてあります。

一時的に org-show-context をオーバーライド

org-match-sparse-tree の実行中、 (org-show-context) を一時的にオーバーライドしても結果は同じです。こちらの方がシンプルでしょうか?

(require 'cl)
(defun org-match-sparse-tree-show-subtree (&optional todo-only match)
  (interactive "P")
  (flet ((org-show-context (&optional key) (org-show-subtree)))
    (org-match-sparse-tree todo-only match)))

fletで一時的にorg-show-contextをオーバーライドしてみました。

マッチしたところだけ後から展開する

これでマッチした全エントリーの中身(サブツリー)を表示出来たのですが通常の動作との使い分けが面倒なことに気がつきました。最初から本文を全部読みたいと思って検索するなら上で定義した (org-match-sparse-tree-show-subtree) を使えば良いのですが「とりあえずタグで検索 → 一覧が出る → 一覧を全部展開」という手順の方が自然な気がします。

本当に欲しかったのはS-TABでマッチした(ハイライトした)エントリだけをvisibility cyclingすることなのではないでしょうか。

マッチした部分は (org-occur-next-match) という関数で巡回出来ます(M-g n や M-g p で移動するときに使われます)。マッチした部分のハイライトは (org-highlight-new-match beg end) で行われています。これは org-occur のためにある関数のようですが指定された範囲にオーバーレイを適用して、オーバーレイを org-occur-highlights リストに追加します。検索後にC-c C-cでハイライトが消えるのは (org-ctrl-c-ctrl-c) 内で org-occur-highlights リストに何か入っているときは (org-remove-occur-highlights) を呼んでいるからだったりします。 (org-occur-next-match) で次のマッチ、前のマッチに移動出来るのはオーバーレイの 'org-type プロパティに値 'org-occur が設定されているからで、その値を検索して移動しているようです。

なので (org-occur-next-match) でハイライト部分を巡回して (org-show-subtree) しまくればマッチしたエントリーの内容をサブツリーを含めて表示出来ます。

(defun org-show-occur-highlights-subtree ()
  (interactive)
  (save-excursion
    (goto-char (point-min))

    (while (ignore-errors (org-occur-next-match 1))
      (org-show-subtree))))

本当は S-TAB をオーバーライドしてハイライトがあるときはハイライトされている部分だけを展開したり折りたたんだりできればいいのですが現在の展開状態を調べる必要があるので面倒そうなので今日はここまでにしておきます。

(2020-04-16追記)S-TABでマッチしたエントリのみを展開したり閉じたりする

ここまでで一応検索結果(Sparse Tree)の本文を一括で表示(展開)できるようになったのですが、やっぱり S-TAB に割り当てて検索結果を開いたり閉じたりしたい! ということで引き続き調べてみました。

まず、既存の S-TAB がどのようになっているのか調べる必要があります。

調べてみた結果、次のようなフローになっていることが分かりました。複雑ですね。

S-TAB => org-shifttab => org-global-cycle => (org-cycle '(4)) => org-cycle-internal-global

C-u 引数によって多少流れが変わることもあるかもしれませんが最終的には org-cycle-internal-global が呼ばれるようです。

org-cycle-internal-global関数は切り替えの処理を行いますが最初はOVERVIEWで表示し、同じコマンドが連続して呼ばれたときに => CONTENTS => ALL => OVERVIEW … の順で表示を切り替える実装になっています。私はこれまで気がついていなかったのですが現在の状態から次の状態へ変わるわけではないんですね。例えば他の作業をした後にS-TABを3回押せばどんな状態からでも必ずALLになるわけです。

実際の表示状態の変更は次の関数が行います。

org-overview
トップレベルのヘッドラインだけ表示
org-content
全てのヘッドラインだけを表示
org-show-all
内容も含めて全て表示(ドロワーなど一部を除く)

ハイライトがあるとき(Sparse Tree表示時)の動作はこれらの関数を修正すべきでしょう。S-TABだけでなくこれらの関数を直接呼び出すこともあるでしょうし。

……と思ったのですが、これらの関数がハイライトがあるときにどのような動作をすべきか考えたのですがなかなか一貫性のある挙動を決めることができませんでした。

org-overview
このまま? ハイライトされている部分だけを表示?(デフォルトの表示って事?)
org-content
ハイライトされているエントリ以下の全てのヘッドラインを表示? org-occurでマッチしたヘッドライン以外の部分はどうする?
org-show-all
ハイライトされているエントリ以下の全てを表示?

org-occurがあるのでハイライトされるのはヘッドラインだけとは限らないところが問題を難しくします。Sparse Treeを表示した直後に1回S-TABを押したときに同じ表示になる(何も変わらない)のも避けたいところです。

仕方が無いので既存の動作との一貫性は脇に置いてSparse Tree表示時の特殊な挙動として仕様を決めることにしました。

org-sparse-tree-show-matched
マッチしたところを表示(デフォルト)
org-sparse-tree-show-all
サブツリーを含めて全て表示

ハイライトがあるときは、この二つと既存の三つをS-TABで切り替えることにします。

OVERVIEW => SPARSE-TREE-SHOW-MATCHED => SPARSE-TREE-SHOW-ALL => CONTENTS => ALL

つまり2回S-TABすればマッチした箇所(タグ検索の場合はヘッドラインのみ)が、3回S-TABすれば本文まで展開してくれるわけです。既存の操作感覚に割と近くなっていると思います。マッチした部分に限定されたくない場合に備えて、4回S-TABすればCONTENTS、5回S-TABすればALLとなるようにしました。

;; org-sparse-tree-cycle.el
;; https://misohena.jp/blog/2020-04-15-show-contents-of-entry-when-sparse-tree-in-org-mode.html
;;

(defun org-sparse-tree-show-all ()
  "マッチした部分をサブツリーを含めて表示します。"
  (interactive)
  (save-excursion
    (org-overview)

    (goto-char (point-min))
    (while (ignore-errors (org-occur-next-match 1))
      (org-show-context 'tags-tree)
      (org-show-subtree))))

(defun org-sparse-tree-show-matched ()
  "マッチした部分を表示します。"
  (interactive)
  (save-excursion
    (org-overview)

    (goto-char (point-min))
    (while (ignore-errors (org-occur-next-match 1))
      (org-show-context 'tags-tree))))

;; この関数は org-cycle-internal-global を元に修正しています。
(defun org-sparse-tree-cycle-internal ()
  "Do the sparse tree cycling action."
  ;; Rotate overview => sparse-tree-show-matched => sparse-tree-show-all => contents => show all

  ;; Hack to avoid display of messages for .org  attachments in Gnus
  (let ((ga (string-match "\\*fontification" (buffer-name))))
    (cond
     ((and (eq last-command this-command)
           (eq org-cycle-global-status 'sparse-tree-show-all))
      ;; We just created the overview - now do table of contents
      ;; This can be slow in very large buffers, so indicate action
      (run-hook-with-args 'org-pre-cycle-hook 'contents)
      (unless ga (org-unlogged-message "CONTENTS..."))
      (org-content)
      (unless ga (org-unlogged-message "CONTENTS...done"))
      (setq org-cycle-global-status 'contents)
      (run-hook-with-args 'org-cycle-hook 'contents))

     ((and (eq last-command this-command)
           (eq org-cycle-global-status 'contents))
      ;; We just showed the table of contents - now show everything
      (run-hook-with-args 'org-pre-cycle-hook 'all)
      (org-show-all '(headings blocks))
      (unless ga (org-unlogged-message "SHOW ALL"))
      (setq org-cycle-global-status 'all)
      (run-hook-with-args 'org-cycle-hook 'all))

     ((and (eq last-command this-command)
      ;; sparse tree show matched
           (eq org-cycle-global-status 'overview))
      (org-sparse-tree-show-matched)
      (setq org-cycle-global-status 'sparse-tree-show-matched))

     ((and (eq last-command this-command)
           (eq org-cycle-global-status 'sparse-tree-show-matched))
      ;; sparse tree show all
      (org-sparse-tree-show-all)
      (setq org-cycle-global-status 'sparse-tree-show-all))

     (t
      ;; overview
      (run-hook-with-args 'org-pre-cycle-hook 'overview)
      (org-overview)
      (unless ga (org-unlogged-message "OVERVIEW"))
      (setq org-cycle-global-status 'overview)
      (run-hook-with-args 'org-cycle-hook 'overview)))))

(defun org-sparse-tree-shifttab-advice (oldfun arg)
  (if org-occur-highlights
      ;; ハイライトがある
      (org-sparse-tree-cycle-internal)
    ;; 元の関数を呼ぶ
    (funcall oldfun arg)))

;; org-shifttabをオーバーライドする
(advice-add 'org-shifttab :around #'org-sparse-tree-shifttab-advice)

うん、これでSparse Treeが大分使いやすくなりました。