org-modeで書かれた文書を org-match-sparse-tree
を使ってをタグで検索する(Sparse Tree を作る)とマッチしたヘッドラインだけが表示されるのですが、そのとき本文も全て一括で表示して欲しかったのでどうしたらよいのか調べてみました。
例えば DIARY タグで検索して日記を通して読みたいときにいちいち一つ一つのヘッドラインをTABキーで展開してまわるのは面倒です。かといってS-TABだとマッチしていない部分の本文も展開されてしまいます。
マニュアルのSparse Treeの項目を見てもマッチしたエントリを移動する操作くらいしか書いていませんでした。
しかたがないので 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が大分使いやすくなりました。