Using consult and org-ql to search my Org Mode agenda files and sort the results to prioritize heading matches
| emacs, orgI want to get better at looking in my Org files for something that I don't exactly remember. I might remember a few words from it but not in order, or I might remember some words from the body, or I might need to fiddle with the keywords until I find it.
I usually use C-u C-c C-w
(org-refile
with a prefix argument),
counting on consult + orderless to let me just put in keywords in any
order. This doesn't let me search the body, though.
org-ql seems like a great fit for this. It's fast and flexible, and might be useful for all sorts of queries.
I think by default org-ql matches against all of the text in the
entry. You can scope the match to just the heading with a query like
heading:your,text
. I wanted to see all matches, prioritize heading
matches so that they come first. I thought about saving the query by
adding advice before org-ql-search
and then adding a new comparator
function, but that got a bit complicated, so I haven't figured that
out yet. It was easier to figure out how to rewrite the query to use
heading
instead of rifle
, do the more constrained query, and then
append the other matches that weren't in the heading matches.
Also, I wanted something a little like helm-org-rifle
's live
previews. I've used helm before, but I was curious about getting it to
work with consult.
Here's a quick demo of my-consult-org-ql-agenda-jump
, which I've
bound to M-s a
. The top few tasks have org-ql in the heading, and
they're followed by the rest of the matches. I think this might be handy.
(defun my-consult-org-ql-agenda-jump () "Search agenda files with preview." (interactive) (let* ((marker (consult--read (consult--dynamic-collection #'my-consult-org-ql-agenda-match) :state (consult--jump-state) :category 'consult-org-heading :prompt "Heading: " :sort nil :lookup #'consult--lookup-candidate)) (buffer (marker-buffer marker)) (pos (marker-position marker))) ;; based on org-agenda-switch-to (unless buffer (user-error "Trying to switch to non-existent buffer")) (pop-to-buffer-same-window buffer) (goto-char pos) (when (derived-mode-p 'org-mode) (org-fold-show-context 'agenda) (run-hooks 'org-agenda-after-show-hook)))) (defun my-consult-org-ql-agenda-format (o) (propertize (org-ql-view--format-element o) 'consult--candidate (org-element-property :org-hd-marker o))) (defun my-consult-org-ql-agenda-match (string) "Return candidates that match STRING. Sort heading matches first, followed by other matches. Within those groups, sort by date and priority." (let* ((query (org-ql--query-string-to-sexp string)) (sort '(date reverse priority)) (heading-query (-tree-map (lambda (x) (if (eq x 'rifle) 'heading x)) query)) (matched-heading (mapcar #'my-consult-org-ql-agenda-format (org-ql-select 'org-agenda-files heading-query :action 'element-with-markers :sort sort))) (all-matches (mapcar #'my-consult-org-ql-agenda-format (org-ql-select 'org-agenda-files query :action 'element-with-markers :sort sort)))) (append matched-heading (seq-difference all-matches matched-heading)))) (use-package org-ql :bind ("M-s a" . my-consult-org-ql-agenda-jump))
Along the way, I learned how to use consult to complete using
consult--dynamic-collection
and add consult--candidate
so that I
can reuse consult--lookup-candidate
and consult--jump-state
. Neat!
Someday I'd like to figure out how to add a sorting function and sort by headers without having to reimplement the other sorts. In the meantime, this might be enough to help me get started.