Org QL Notes
[#A] Contents
[#A] Overview
Bugs (view)
| P | M | Keyword | Heading |
|---|---|---|---|
| NEXT | Check Org 9.4 source code for second argument | ||
| A | 0.6 | PROJECT | Compatibility with Org 9.4 custom link changes |
Milestones (view)
| M | P | Todo | Heading |
|---|---|---|---|
| 0.6 | A | PROJECT | Compatibility with Org 9.4 custom link changes |
| 0.6 | A | PROJECT | Reverse sorting |
| future | B | TODO | Sorter for property values |
| 0.7 | B | TODO | clocked and closed’s behavior with just-number args”>Change clocked and closed’s behavior with just-number args |
| 0.6 | B | TODO | Review all normalizers for potential loops |
| 0.6 | B | TODO | Normalize query while preserving body |
| 0.6 | B | TODO | Use string queries in view headers when possible |
| 0.7 | B | PROJECT | Optimized, date-specific timestamp regexps |
Underway (view)
| M | P | Keyword | Heading |
|---|---|---|---|
| 0.7 | B | PROJECT | Optimized, date-specific timestamp regexps |
| 0.7 | B | PROJECT | Group tag support |
| UNDERWAY | ~clocked~ | ||
| UNDERWAY | ~closed~ | ||
| UNDERWAY | ~deadline~ | ||
| UNDERWAY | ~planning~ | ||
| UNDERWAY | ~scheduled~ | ||
| UNDERWAY | ~ts~ | ||
| UNDERWAY | Benchmarking tags searches without and with new group-tags support |
To-do (view)
| P | M | Todo | Heading |
|---|---|---|---|
| A | 0.6 | PROJECT | Compatibility with Org 9.4 custom link changes |
| A | PROJECT | Convert simple sexp queries to non-sexp | |
| A | 0.6 | PROJECT | Reverse sorting |
| A | PROJECT | Org link types {2/3} |
Stuck projects (view)
| M | P | Heading |
|---|---|---|
| A | Convert simple sexp queries to non-sexp | |
| 0.6 | A | Reverse sorting |
| A | Org link types {2/3} | |
| B | Outline path predicate | |
| B | Document the sorting functions | |
| future | B | Recursive queries |
| future | B | Timeline view |
[#A] Tasks
- Sorter for property values
- Add :auto keyword to (planning) predicate
- Bookmarks save list of files instead of e.g. org-agenda-files when appropriate
- Change (deadline)’s auto argument to :auto and/or :auto t
- Outline path in buffers-files arg
- Add more sorters?
- Default sort
- Partial match for property queries
- Remove org-ql macro
- Change clocked and closed’s behavior with just-number args
- Review all normalizers for potential loops
- Normalize query while preserving body
- Example: Next upcoming event
- org-agenda-skip-function
- Update commentary
- org-ql-block: Let org-agenda format its output
- OmniFocus-like screencasts
- Use ripgrep to search unopened files
- Key cache results by action function
- Use session-async to have a persistent search process
- Overlay-based caching inspired by org-num-mode
- Fancier searching for inherited tags
- Compatibility with Org 9.4 custom link changes
- Convert simple sexp queries to non-sexp
- Reverse sorting
- Recursive sub-queries in non-sexp format
- Optimized, date-specific timestamp regexps
- Multi-pass query normalization
- Optimization for olp predicate
- Outline path predicate
- Tools for saving queries and accessing them {3/4}
- Group tag support
- Document the sorting functions
- Recursive queries
- Timeline view
- Implement view with tabulated-list-mode or magit-section
- Dynamic blocks
- Predicate helper functions
- New Transient transient-lisp-variable class
- Normalize queries
- Update view screenshots
- Test caching
- Alternative parsing libraries
- Timestamp predicates using relative dates break caching
- ~(link :target)~ doesn’t work
- ~(link :regexp-p)~ doesn’t work
- Fix: Custom sorter breaks cached results
- Update dash dependencies
- Checking links for unsafe parameters
- Views: Multiple sorters are not preserved
- Make dynamic blocks warn about sexp queries
- Add Emacs 27.1 to test.yml
- Fix org-ql-view–link-open on Org 9.3+
- Fix query-sexp-to-string function’s handling of, e.g. descendants
- Helm command
- Add a :with-time argument to timestamp predicates
- “Node” caching
- Define predicates with a macro
- Move this notes file into an orphan meta/notes branch
- Quickly change sorting/grouping in search views
- Byte-compile lambdas
- Document/figure out tag inheritance
- Dual matching with regexp and predicates
- Operate on list of heading positions
- Use macros for date
[#B] Sorter for property values
[2021-06-18 Fri 02:26] e.g. to sort entries in this file by the milestone property. Something like:
(org-ql-search (current-buffer)
'(todo)
:sort '((property "milestone") priority))[#B] Add :auto keyword to (planning) predicate
It should act like (or (deadline auto) (scheduled :to today)).
[#B] Bookmarks save list of files instead of e.g. org-agenda-files when appropriate
[2020-12-05 Sat 01:24] The bookmarked plist should use the “contracted” form rather than the list of files so that, if the list of files changes before the bookmark is loaded, it will use the new list of files.
[2021-06-18 Fri 03:12] This has the potential to cause bugs, so I’m going to defer it until at least 0.7.
[#B] Change (deadline)’s auto argument to :auto and/or :auto t
For consistency, because plain auto looks like a variable, and even though it’s in a quoted form, it could be confusing.
[#B] Outline path in buffers-files arg
e.g.
(org-ql (olp "~/org/inbox.org" "Emacs" "Ideas")
(todo "NEXT"))Also, should support an id one.
[#B] Add more sorters?
- [ ]
category
- [ ] Any date
- e.g. it would search for timestamps (active/inactive?) anywhere in an entry
[#B] Default sort
Would probably be useful to have a default sort option.
[#B] Partial match for property queries
e.g. something like Organice has now.
[2020-02-13 Thu 00:42] Something is needed to help search property values by partial matches. For example:
* [[https://github.com/fniessen/org-html-themes][org-html-themes: Framework including two themes, Bigblow and ReadTheOrg]]
,:PROPERTIES:
,:author: Fabrice Niessen
,:END:
Searching that with a query like property:author=Fabrice returns nothing; the full value must be used, like property:author="Fabrice Niessen"~. It should be possible to do something like ~property:author=~Fabrice to search for partial matches.
Remove org-ql macro
0.6 will be the last stable release to have it.
[#B] Change clocked and closed’s behavior with just-number args
;; TODO: While it seems helpful for (clocked) and (close) to
;; implicitly look into the past (because entries can't be
;; clocked or closed in the future), it makes the API
;; inconsistent. It would be better to be consistent and
;; require the user to pass these predicates a negative number.[#B] Review all normalizers for potential loops
Since I found (and fixed) one after I made normalizers apply repeatedly, I should check the rest, because there might be a few more that could do it.
[#B] Normalize query while preserving body
Sometimes it would be helpful to define a predicate that normalizes to another predicate while having a different body. For example:
(org-ql-defpred created (&key from to on)
"Search for entries with \"CREATED\" property in range or on date"
:body (if (and on (or from to))
(error "Either specify FROM and/or TO, or ON")
(let ((heading-time (pcl/as-ts (org-entry-get (point) "CREATED")))
(on (pcl/as-ts on))
(from (pcl/as-ts (or on from)))
(to (pcl/as-ts (or on to))))
(and t
(if from (ts<= from heading-time) t)
(if to (ts< heading-time (ts-adjust 'day +1 to)) t)))))That predicate would best be normalized to (property "CREATED"), but its body would still need to be evaluated to compare the property value as a timestamp. There are two obvious possibilities:
- Normalize it to
(and (property "CREATED") (...form that compares (org-entry-get (point) "CREATED") as a timestamp...)), without a defined body. - Allow
-defpredto normalize the query to(property "CREATED")while preserving a separate body. Maybe something like normalizing it to(and (property "CREATED") (created ...)), which would still call the body; however, the normalizer must avoid looping on(created ...), so maybe a sentinel value is needed.
[#C] Example: Next upcoming event
[2020-11-26 Thu 18:17] Inspired by GitHub - unhammer/org-upcoming-modeline: put upcoming org event in modeline (Reddit thread).
(pcase-let* ((now (ts-now))
(items (org-ql-select (org-agenda-files)
'(ts-active :from 0 :to 1)
:action '(cons (save-excursion
(car (sort (cl-loop while (re-search-forward org-tsr-regexp nil t)
collect (ts-parse-org (match-string 1)))
#'ts<)))
(point-marker))))
(`(,time . ,marker) (car (seq-sort-by #'car #'ts< items)))
(heading (org-with-point-at marker
(org-link-display-format (nth 4 (org-heading-components)))))
(seconds-until (ts-difference time now))
;; NOTE: Using day of year to avoid end-of-month turnover in day number.
(days-until (- (ts-doy time) (ts-doy now)))
(time-string (cond ((<= seconds-until org-upcoming-modeline-duration-threshold)
(ts-human-format-duration seconds-until 'abbreviate))
((= 0 days-until)
(ts-format "%H:%M" time))
((= 1 days-until)
(ts-format "tomorrow %H:%M" time))
(t ; > 1 days-until
(ts-format "%a %H:%M" time)))))
(propertize (format " ⏰ %s: %s" time-string heading)
'face 'org-level-4
'help-echo (format "%s at <%s>" heading
(ts-format "%Y-%m-%d %H:%M" time))))[#C] org-agenda-skip-function
As discussed here, this is a cool feature that allows further integration into existing custom agenda commands. Example:
;;; lima-0ac22.el --- -*- lexical-binding: t; -*-
(defun org-ql-skip-function (query)
"Return a function for `org-agenda-skip-function' for QUERY.
Compared to using QUERY in `org-ql', this effectively turns QUERY
into (not QUERY)."
(let* ((predicate (org-ql--query-predicate '(regexp "ryo-modal"))))
(lambda ()
;; This duplicates the functionality of `org-ql--select'.
(let (orig-fns)
(--each org-ql-predicates
;; Save original function mappings.
(let ((name (plist-get it :name)))
(push (list :name name :fn (symbol-function name)) orig-fns)))
(unwind-protect
(progn
(--each org-ql-predicates
;; Set predicate functions.
(fset (plist-get it :name) (plist-get it :fn)))
;; Run query.
;; FIXME: "If this function returns nil, the current match should not be skipped.
;; Otherwise, the function must return a position from where the search
;; should be continued."
(funcall predicate))
(--each orig-fns
;; Restore original function mappings.
(fset (plist-get it :name) (plist-get it :fn))))))))
(let ((org-agenda-custom-commands
'(("z" "Z"
((tags-todo "PRIORITY=\"A\"+Emacs/!SOMEDAY"))
((org-agenda-skip-function (org-ql-skip-function '(regexp "ryo-modal")))))
((org-agenda-files ("~/org/inbox.org"))))))
(org-agenda nil "z"))I should benchmark it to see how much difference it makes, because all those fset calls on each heading isn’t free. But if a macro were used to rewrite the built-in predicates to their full versions, all of that could be avoided…
[#C] Update commentary
MAYBE org-ql-block: Let org-agenda format its output
[2020-11-16 Mon 22:12] As suggested by Kevin J. Foley. See #113. It might actually be simple to do and might work very well.
MAYBE OmniFocus-like screencasts
[2020-11-18 Wed 03:03] Looking at OmniFocus’s web site now, their short videos showing features demonstrate a lot of features that exist in org-ql already. After implementing section-based views, it would be cool to make some short demos showing similar features.
MAYBE Use ripgrep to search unopened files
[2020-11-26 Thu 02:20] It supports multiline search now, so it might be suitable now.
MAYBE Key cache results by action function
Could allow reusing results for different queries. Mentioned in Reddit discussion.
MAYBE Use session-async to have a persistent search process
That could run queries in the other process and then send results to the main Emacs process.
MAYBE [#C] Overlay-based caching inspired by org-num-mode
[2019-12-30 Mon 22:42] Newer versions of Org have org-num-mode, which uses font-lock and after-change-functions to update overlays in the buffer with outline numbering. Maybe a similar approach could be used to cache arbitrary values for headings in a buffer without having to discard the whole buffer’s cache when the buffer changes.
[2020-11-09 Mon 01:51] I feel like that’s probably unlikely to work well. I imagine it would require storing the query at every heading, which would be very wasteful. As well, adding more overlays to an Org buffer is probably not a good idea, because there are already enough of those.
However, there might still be a useful idea here somewhere…
MAYBE [#C] Fancier searching for inherited tags
When tag inheritance is enabled, and the given tags aren’t file-level tags, we could search directly to headings containing the matching tags, and then only do per-heading matching on the subtrees. Sometimes that would be much faster. However, that might make the logic special-cased and complicated. Might need a redesign of the whole matching/predicate system to do cleanly.
PROJECT [#A] Compatibility with Org 9.4 custom link changes
[2020-11-13 Fri 22:36] From the changelog:
Calling conventions changes when opening or exporting custom links
This changes affects export back-ends, and libraries providing new link types.
Function used in :follow link parameter is required to accept a second argument. Likewise, function used in :export parameter needs to accept a fourth argument. See org-link-set-parameters for details.
Eventually, the function org-export-custom-protocol-maybe is now called with a fourth argument. Even though the 3-arguments definition is still supported, at least for now, we encourage back-end developers to switch to the new signature.
Unfortunately it does not say what the new, required second argument is.
[2020-11-22 Sun 17:22] For now, I’ll add an optional, ignored second argument to the follow function; if I’m lucky, it will work anyway.
NEXT Check Org 9.4 source code for second argument
PROJECT [#A] Convert simple sexp queries to non-sexp
[2020-11-11 Wed 00:28] This will be very helpful for storing links. Surely simple ones won’t be too hard…
(defun org-ql--query-sexp-to-plain (query)
"Return a plain query string for sexp QUERY.
If QUERY can't be converted to a plain one, return nil."
;; This started out pretty simple...but at least it's not just one long function, right?
(cl-labels ((complex-p (query)
(or (contains-p 'or query)))
(contains-p (symbol list)
(cl-loop for element in list
thereis (or (eq symbol element)
(and (listp element)
(contains-p symbol element)))))
(format-args
(args) (let (non-paired paired next-keyword)
(cl-loop for arg in args
do (cond (next-keyword (push (cons next-keyword arg) paired)
(setf next-keyword nil))
((keywordp arg) (setf next-keyword (substring (symbol-name arg) 1)))
(t (push arg non-paired))))
(string-join (append (mapcar #'format-atom non-paired)
(nreverse (--map (format "%s=%s" (car it) (cdr it))
paired)))
",")))
(format-atom
(atom) (cl-typecase atom
(string (if (string-match (rx space) atom)
(format "%S" atom)
(format "%s" atom)))
(t (format "%s" atom))))
(format-form
(form) (pcase form
(`(not . (,rest)) (concat "!" (format-form rest)))
(`(priority . ,_) (format-priority form))
;; FIXME: Convert (src) queries to non-sexp form...someday...
(`(src . ,_) (user-error "Converting (src ...) queries to non-sexp form is not implemented"))
(_ (pcase-let* ((`(,pred . ,args) form)
(args-string (pcase args
('() "")
((guard (= 1 (length args))) (format "%s" (car args)))
(_ (format-args args)))))
(format "%s:%s" pred args-string)))))
(format-and
(form) (pcase-let* ((`(and . ,rest) form))
(string-join (mapcar #'format-form rest) " ")))
(format-priority
(form) (pcase-let* ((`(priority . ,rest) form)
(args (pcase rest
(`(,(and comparator (or < <= > >= =)) ,letter)
(priority-letters comparator letter))
(_ rest))))
(concat "priority:" (string-join args ","))))
(priority-letters
(comparator letter) (let* ((char (string-to-char (upcase (symbol-name letter))))
(numeric-priorities '(?A ?B ?C))
;; NOTE: The comparator inversion is intentional.
(others (pcase comparator
('< (--select (> it char) numeric-priorities))
('<= (--select (>= it char) numeric-priorities))
('> (--select (< it char) numeric-priorities))
('>= (--select (<= it char) numeric-priorities))
('= (--select (= it char) numeric-priorities)))))
(mapcar #'char-to-string others))))
(unless (complex-p query)
(pcase query
(`(and . ,_) (format-and query))
(_ (format-form query))))))
(--map (cons it (org-ql--query-sexp-to-plain it))
'((priority >= B)
(priority > B)
(priority < B)
(priority < A)
(priority = A)
(todo)
(todo "TODO")
(todo "TODO" "NEXT")
(ts :from -1 :to 1)
(ts :on today)
(ts-active :from "2017-01-01" :to "2018-01-01")
(heading "quoted phrase" "word")
(and (tags "book" "books") (priority "A"))
(and (tags "space") (not (regexp "moon")))
(src :lang "elisp" :regexps ("defun")))
)[2020-11-11 Wed 01:45] Seems to work well. Now to integrate that into link-saving…
[2020-11-11 Wed 02:41] Seems to work. Will wait for feedback before merging.
[2020-11-11 Wed 19:13] Seems to be working properly. One more thing to do though, I think:
*** TODO [#B] Use string queries in view headers when possible
Maybe make it an option to automatically convert them when possible, because if a user wanted to add complexity to a string query, he’d have to rewrite it as a sexp.
PROJECT [#A] Reverse sorting
Update docs
Need to mention incompatible change (though that isn’t the best way to describe it) in the changelog.
WAITING Get feedback
PROJECT Recursive sub-queries in non-sexp format
[2021-09-08 Wed 15:41] Eric Abrahamsen showed me this in #emacs:matrix.org:
(defvar test-pexs
'((query (+ (or compound-term term)))
(term (or subquery prefixed-term kv-term value) term-end)
(subquery "(" query ")"
`(query -- (if (= 1 (length query)) query (list query))))
(prefixed-term (or negated-term near-term))
(negated-term (or "not " "-") term
`(term -- (list 'not term)))
(near-term "near " term
`(term -- (list 'near term)))
(compound-term (or or-terms and-terms))
(or-terms (or subquery prefixed-term term) "or " (or subquery prefixed-term term)
`(t1 t2 -- (list 'or t1 t2)))
(and-terms (or subquery prefixed-term term) "and " (or subquery prefixed-term term)
`(t1 t2 -- (list 'and t1 t2)))
(value (or quoted-value plain-value))
(plain-value (substring (+ [word])))
(quoted-value "\"" (substring (+ (not "\"") (any))) "\"")
(kv-term plain-value ":" value
`(k v -- (cons (intern k) v)))
(term-end (opt (+ [space])))))The INFINITE RECURSIVE SUBQUERIES is particularly nice
BTW, I see what you mean about the peg macros, that’s a real pain. I have a small patch that should fix that
This was half-cribbed from org-ql to begin with, so it’s only right that you get something in return :)
The subtlety is all in the ordering of the or statements
It should be pretty easy to add some of that to our query parsing.
PROJECT [#B] Optimized, date-specific timestamp regexps
- In branch
wip/ts-optimized-regexps.
[2020-11-28 Sat 19:12] For example, a regexp like this would make timestamp searches very fast:
(rx "<" "2020" "-" (or "10-31" "11-01" "11-02") ">")
;;=> "<2020-\\(?:\\(?:1\\(?:0-31\\|1-0[12]\\)\\)\\)>"It shouldn’t be hard to generate a list of date strings that rx-to-string could optimize, and that would avoid parsing and testing many timestamps which wouldn’t match when specific date ranges are given.
Limit or optimize
[2020-12-22 Tue 04:55] Noticed that the tests affected by this change are now slower in the test suite, presumably because they span a range of years and spend more time incrementing ts structs in the regexp-building function than running the search on the small amount of test data. For smaller date ranges, and for searching larger sets of data, the performance is improved. So there needs to be some kind of heuristic to handle this. Or maybe the regexp-building function could be smarter rather than “brute-forcing” its way through every date in the range.
(This also shows how valuable Buttercup’s showing of each test’s duration is, otherwise I might not have noticed this issue.)
inactive
without arguments (preamble) (2.43ms)
without arguments (no preamble) (2.58ms)
:from a timestamp (preamble) (421.88ms)
:from a timestamp (no preamble) (8.65ms)
:from a number of days (preamble) (20.77ms)
:from a number of days (no preamble) (3.06ms)
:to a timestamp (preamble) (424.83ms)
:to a timestamp (no preamble) (8.48ms)
:to a number of days (preamble) (220.44ms)
:to a number of days (no preamble) (2.68ms)
:on a timestamp (preamble) (9.51ms)
:on a timestamp (no preamble) (8.03ms)
:on a number of days (preamble) (67.11ms)
:on a number of days (no preamble) (3.25ms)
[2020-12-22 Tue 19:16] It should be simple to do something like this:
- If the range spans at least a year, include every day and month number, and each year number.
- If the range spans less than a year but at least a month, include every day number, each month number, and the year number(s).
- If the range spans less than a month, probably just increment and collect the numbers, like the code currently does (which will ensure the proper result in case the range spans the end of a month and/or year).
If necessary, another step could be to divide the range into three parts: the middle being the part that spans whole months/years, and then the ends being the parts that span partial months/years. Then a regexp could be made for each part and combined into a single regexp which would match each part separately (rather than combining all numbers into one potential timestamp).
UNDERWAY clocked
Tests
Use optimized regexp in predicate
Benchmark
(bench-multi-lexical :times 1 :ensure-equal t
:forms (("unoptimized"
(progn
(org-ql-defpred clocked (&key from to _on)
;; The underscore before `on' prevents "unused lexical variable"
;; warnings, because we pre-process that argument in a macro before
;; this function is called.
"Return non-nil if current entry was clocked in given period.
If no arguments are specified, return non-nil if entry has any
timestamp.
If FROM, return non-nil if entry has a timestamp on or after
FROM.
If TO, return non-nil if entry has a timestamp on or before TO.
If ON, return non-nil if entry has a timestamp on date ON.
FROM, TO, and ON should be either `ts' structs, or strings
parseable by `parse-time-string' which may omit the time value."
:normalizers ((`(,predicate-names ,(and num-days (pred numberp)))
;; (clocked) and (closed) implicitly look into the past.
(let ((from (->> (ts-now)
(ts-adjust 'day (* -1 num-days))
(ts-apply :hour 0 :minute 0 :second 0))))
`(clocked :from ,from))))
:preambles ((`(,predicate-names ,(pred numberp))
(list :regexp org-ql-clock-regexp :query t))
(`(,predicate-names)
(list :regexp org-ql-clock-regexp :query t)))
:body
(org-ql--predicate-ts :from from :to to :regexp org-ql-clock-regexp :match-group 1))
(setf org-ql-cache (make-hash-table :weakness 'key))
(org-ql-select (org-agenda-files)
'(clocked :from "2020-01-01" :to "2020-12-31")
:action '(substring-no-properties (org-get-heading t t)))))
("optimized"
(progn
(org-ql-defpred clocked (&key from to _on)
;; The underscore before `on' prevents "unused lexical variable"
;; warnings, because we pre-process that argument in a macro before
;; this function is called.
"Return non-nil if current entry was clocked in given period.
If no arguments are specified, return non-nil if entry has any
timestamp.
If FROM, return non-nil if entry has a timestamp on or after
FROM.
If TO, return non-nil if entry has a timestamp on or before TO.
If ON, return non-nil if entry has a timestamp on date ON.
FROM, TO, and ON should be either `ts' structs, or strings
parseable by `parse-time-string' which may omit the time value."
:normalizers
((`(,predicate-names ,(and num-days (pred numberp)))
;; (clocked) and (closed) implicitly look into the past.
(let ((from (->> (ts-now)
(ts-adjust 'day (* -1 num-days))
(ts-apply :hour 0 :minute 0 :second 0))))
`(clocked :from ,from))))
:preambles
((`(,predicate-names ,(pred numberp))
(list :regexp org-ql-clock-regexp :query t))
(`(,predicate-names . ,(and rest (guard (or (plist-get rest :from)
(plist-get rest :to)
(plist-get rest :on)))))
;; Use date-optimized timestamp regexp.
(-let (((&plist :from :to :on :type) rest))
(org-ql--from-to-on)
(list :regexp (-let* ((from (or from (ts-adjust 'day (- org-ql-ts-days-from-default) (ts-now))))
(to (or to (ts-adjust 'day org-ql-ts-days-to-default (ts-now))))
(ts-regexp (org-ql--ts-range-to-regexp from to :type 'inactive)))
(rx-to-string `(seq bol (0+ blank) "CLOCK:" (1+ blank) (0+ not-newline) (regexp ,ts-regexp))))
:query query)))
(`(,predicate-names)
(list :regexp org-ql-clock-regexp :query t)))
:body
(org-ql--predicate-ts :from from :to to :regexp org-ql-clock-regexp :match-group 1))
(setf org-ql-cache (make-hash-table :weakness 'key))
(org-ql-select (org-agenda-files)
'(clocked :from "2020-01-01" :to "2020-12-31")
:action '(substring-no-properties (org-get-heading t t)))))))
| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| optimized | 1.85 | 1.746526 | 0 | 0 |
| unoptimized | slowest | 3.239420 | 0 | 0 |
UNDERWAY closed
Tests
Use optimized regexp in predicate
Benchmark
(bench-multi-lexical :times 1 :ensure-equal t
:forms (("unoptimized"
(progn
(org-ql-defpred closed (&key from to _on)
;; The underscore before `on' prevents "unused lexical variable"
;; warnings, because we pre-process that argument in a macro before
;; this function is called.
"Return non-nil if current entry was closed in given period.
If no arguments are specified, return non-nil if entry has any
timestamp.
If FROM, return non-nil if entry has a timestamp on or after
FROM.
If TO, return non-nil if entry has a timestamp on or before TO.
If ON, return non-nil if entry has a timestamp on date ON.
FROM, TO, and ON should be either `ts' structs, or strings
parseable by `parse-time-string' which may omit the time value."
:normalizers ((`(,predicate-names ,(and num-days (pred numberp)))
;; (clocked) and (closed) implicitly look into the past.
(let ((from (->> (ts-now)
(ts-adjust 'day (* -1 num-days))
(ts-apply :hour 0 :minute 0 :second 0))))
`(closed :from ,from))))
:preambles ((`(,predicate-names . ,_)
;; Predicate still needs testing.
(list :regexp org-closed-time-regexp :query query)))
:body
(org-ql--predicate-ts :from from :to to :regexp org-closed-time-regexp :match-group 1
:limit (line-end-position 2)))
(setf org-ql-cache (make-hash-table :weakness 'key))
(org-ql-select (org-agenda-files)
'(closed :from "2020-01-01" :to "2020-12-31")
:action '(substring-no-properties (org-get-heading t t)))))
("optimized"
(progn
(org-ql-defpred closed (&key from to _on)
;; The underscore before `on' prevents "unused lexical variable"
;; warnings, because we pre-process that argument in a macro before
;; this function is called.
"Return non-nil if current entry was closed in given period.
If no arguments are specified, return non-nil if entry has any
timestamp.
If FROM, return non-nil if entry has a timestamp on or after
FROM.
If TO, return non-nil if entry has a timestamp on or before TO.
If ON, return non-nil if entry has a timestamp on date ON.
FROM, TO, and ON should be either `ts' structs, or strings
parseable by `parse-time-string' which may omit the time value."
:normalizers
((`(,predicate-names ,(and num-days (pred numberp)))
;; (clocked) and (closed) implicitly look into the past.
(let ((from (->> (ts-now)
(ts-adjust 'day (* -1 num-days))
(ts-apply :hour 0 :minute 0 :second 0))))
`(closed :from ,from))))
:preambles
((`(,predicate-names . ,(and rest (guard (or (plist-get rest :from)
(plist-get rest :to)
(plist-get rest :on)))))
(-let (((&plist :from :to :on :type) rest))
(org-ql--from-to-on)
(list :regexp (-let* ((from (or from (ts-adjust 'day (- org-ql-ts-days-from-default) (ts-now))))
(to (or to (ts-adjust 'day org-ql-ts-days-to-default (ts-now))))
(ts-regexp (org-ql--ts-range-to-regexp from to :type 'inactive)))
(rx-to-string `(seq bow (0+ blank) "CLOSED:" (1+ blank) (regexp ,ts-regexp))))
:query query)))
(`(,predicate-names . ,_)
;; Predicate still needs testing.
(list :regexp org-closed-time-regexp :query query)))
:body
(org-ql--predicate-ts :from from :to to :regexp org-closed-time-regexp :match-group 1
:limit (line-end-position 2)))
(setf org-ql-cache (make-hash-table :weakness 'key))
(org-ql-select (org-agenda-files)
'(closed :from "2020-01-01" :to "2020-12-31")
:action '(substring-no-properties (org-get-heading t t)))))))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| unoptimized | 1.04 | 0.382831 | 0 | 0 |
| optimized | slowest | 0.396930 | 0 | 0 |
UNDERWAY deadline
Tests
Use optimized regexp in predicate
Benchmark
(bench-multi-lexical :times 1 :ensure-equal t
:forms (("unoptimized"
(progn
(org-ql-defpred deadline (&key from to _on)
;; The underscore before `on' prevents "unused lexical variable"
;; warnings, because we pre-process that argument in a macro before
;; this function is called.
"Return non-nil if current entry has deadline in given period.
If no arguments are specified, return non-nil if entry has any
timestamp.
If FROM, return non-nil if entry has a timestamp on or after
FROM.
If TO, return non-nil if entry has a timestamp on or before TO.
If ON, return non-nil if entry has a timestamp on date ON.
FROM, TO, and ON should be either `ts' structs, or strings
parseable by `parse-time-string' which may omit the time value."
:normalizers ((`(,predicate-names auto)
;; Use `org-deadline-warning-days' as the :to arg.
(let ((to (->> (ts-now)
(ts-adjust 'day org-deadline-warning-days)
(ts-apply :hour 23 :minute 59 :second 59))))
`(deadline-warning :to ,to)))
(`(,predicate-names ,(and num-days (pred numberp)))
(let ((to (->> (ts-now)
(ts-adjust 'day num-days)
(ts-apply :hour 23 :minute 59 :second 59))))
`(deadline :to ,to))))
;; NOTE: Does this normalizer cause the preamble to not be used? (Adding one to the deadline-warning definition to be sure.)
:preambles ((`(,predicate-names . ,_)
(list :regexp org-deadline-time-regexp :query query)))
:body
(org-ql--predicate-ts :from from :to to :regexp org-deadline-time-regexp :match-group 1
:limit (line-end-position 2)))
(setf org-ql-cache (make-hash-table :weakness 'key))
(org-ql-select (org-agenda-files)
'(deadline :from "2020-01-01" :to "2020-12-31")
:action '(substring-no-properties (org-get-heading t t)))))
("optimized"
(progn
(org-ql-defpred deadline (&key from to _on)
;; The underscore before `on' prevents "unused lexical variable"
;; warnings, because we pre-process that argument in a macro before
;; this function is called.
"Return non-nil if current entry has deadline in given period.
If no arguments are specified, return non-nil if entry has any
timestamp.
If FROM, return non-nil if entry has a timestamp on or after
FROM.
If TO, return non-nil if entry has a timestamp on or before TO.
If ON, return non-nil if entry has a timestamp on date ON.
FROM, TO, and ON should be either `ts' structs, or strings
parseable by `parse-time-string' which may omit the time value."
:normalizers
((`(,predicate-names auto)
;; Use `org-deadline-warning-days' as the :to arg.
(let ((to (->> (ts-now)
(ts-adjust 'day org-deadline-warning-days)
(ts-apply :hour 23 :minute 59 :second 59))))
`(deadline-warning :to ,to)))
(`(,predicate-names ,(and num-days (pred numberp)))
(let ((to (->> (ts-now)
(ts-adjust 'day num-days)
(ts-apply :hour 23 :minute 59 :second 59))))
`(deadline :to ,to))))
;; NOTE: Does this normalizer cause the preamble to not be used? (Adding one to the deadline-warning definition to be sure.)
:preambles
((`(,predicate-names . ,(and rest (guard (or (plist-get rest :from)
(plist-get rest :to)
(plist-get rest :on)))))
;; Use date-optimized timestamp regexp.
(-let (((&plist :from :to :on :type) rest))
(org-ql--from-to-on)
(list :regexp (-let* ((from (or from (ts-adjust 'day (- org-ql-ts-days-from-default) (ts-now))))
(to (or to (ts-adjust 'day org-ql-ts-days-to-default (ts-now))))
(ts-regexp (org-ql--ts-range-to-regexp from to :type 'active)))
(rx-to-string `(seq bow (0+ blank) "DEADLINE:" (1+ blank) (regexp ,ts-regexp))))
:query query)))
(`(,predicate-names . ,_)
(list :regexp org-deadline-time-regexp :query query)))
:body
(org-ql--predicate-ts :from from :to to :regexp org-deadline-time-regexp :match-group 1
:limit (line-end-position 2)))
(setf org-ql-cache (make-hash-table :weakness 'key))
(org-ql-select (org-agenda-files)
'(deadline :from "2020-01-01" :to "2020-12-31")
:action '(substring-no-properties (org-get-heading t t)))))))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| unoptimized | 1.54 | 0.258749 | 0 | 0 |
| optimized | slowest | 0.397937 | 0 | 0 |
UNDERWAY planning
Tests
Use optimized regexp in predicate
Benchmark
(bench-multi-lexical :times 1 :ensure-equal t
:forms (("unoptimized"
(progn
(org-ql-defpred planning (&key from to _on)
;; The underscore before `on' prevents "unused lexical variable"
;; warnings, because we pre-process that argument in a macro before
;; this function is called.
"Return non-nil if current entry has planning timestamp in given period (i.e. its deadline, scheduled, or closed timestamp).
If no arguments are specified, return non-nil if entry has any
timestamp.
If FROM, return non-nil if entry has a timestamp on or after
FROM.
If TO, return non-nil if entry has a timestamp on or before TO.
If ON, return non-nil if entry has a timestamp on date ON.
FROM, TO, and ON should be either `ts' structs, or strings
parseable by `parse-time-string' which may omit the time value."
:normalizers ((`(,predicate-names ,(and num-days (pred numberp)))
(let ((to (->> (ts-now)
(ts-adjust 'day num-days)
(ts-apply :hour 23 :minute 59 :second 59))))
`(planning :to ,to))))
:preambles ((`(,predicate-names . ,_)
(list :regexp org-ql-planning-regexp :query query)))
:body
(org-ql--predicate-ts :from from :to to :regexp org-ql-planning-regexp :match-group 1
:limit (line-end-position 2)))
(setf org-ql-cache (make-hash-table :weakness 'key))
(org-ql-select (org-agenda-files)
'(planning :from "2020-01-01" :to "2020-12-31")
:action '(substring-no-properties (org-get-heading t t)))))
("optimized"
(progn
(org-ql-defpred planning (&key from to _on)
;; The underscore before `on' prevents "unused lexical variable"
;; warnings, because we pre-process that argument in a macro before
;; this function is called.
"Return non-nil if current entry has planning timestamp in given period (i.e. its deadline, scheduled, or closed timestamp).
If no arguments are specified, return non-nil if entry has any
timestamp.
If FROM, return non-nil if entry has a timestamp on or after
FROM.
If TO, return non-nil if entry has a timestamp on or before TO.
If ON, return non-nil if entry has a timestamp on date ON.
FROM, TO, and ON should be either `ts' structs, or strings
parseable by `parse-time-string' which may omit the time value."
:normalizers ((`(,predicate-names ,(and num-days (pred numberp)))
(let ((to (->> (ts-now)
(ts-adjust 'day num-days)
(ts-apply :hour 23 :minute 59 :second 59))))
`(planning :to ,to))))
:preambles ((`(,predicate-names . ,(and rest (guard (or (plist-get rest :from)
(plist-get rest :to)
(plist-get rest :on)))))
(-let (((&plist :from :to :on :type) rest))
(org-ql--from-to-on)
(list :regexp (-let* ((from (or from (ts-adjust 'day (- org-ql-ts-days-from-default) (ts-now))))
(to (or to (ts-adjust 'day org-ql-ts-days-to-default (ts-now))))
(ts-regexp (org-ql--ts-range-to-regexp from to)))
(rx-to-string `(seq bow (0+ blank) (or "CLOSED" "DEADLINE" "SCHEDULED") ":"
(1+ blank) (regexp ,ts-regexp))))
:query query)))
(`(,predicate-names . ,_)
(list :regexp org-ql-planning-regexp :query query)))
:body
(org-ql--predicate-ts :from from :to to :regexp org-ql-planning-regexp :match-group 1
:limit (line-end-position 2)))
(setf org-ql-cache (make-hash-table :weakness 'key))
(org-ql-select (org-agenda-files)
'(planning :from "2020-01-01" :to "2020-12-31")
:action '(substring-no-properties (org-get-heading t t)))))))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| optimized | 1.28 | 0.450906 | 0 | 0 |
| unoptimized | slowest | 0.576033 | 0 | 0 |
UNDERWAY scheduled
Benchmark
Tests
Use optimized regexp in predicate
UNDERWAY ts
[2020-12-19 Sat 05:55] Seems to be working well. Not sure how best to integrate with other timestamp-related predicates. The code isn’t that much, so maybe copying it into each predicate would be best.
Tests
Use optimized regexp in predicate
Benchmark
[2020-12-20 Sun 07:51] This is a great improvement. I tested it on the scheduled predicate too, but it doesn’t make nearly as big a difference, because just searching for the SCHEDULED: prefix avoids testing most timestamps.
(bench-multi-lexical :times 1 :ensure-equal t
:forms (("unoptimized"
(progn
(org-ql-defpred (ts ts-active ts-a ts-inactive ts-i)
(&key from to _on regexp (match-group 0) (limit (org-entry-end-position)))
;; NOTE: Arguments to this predicate are pre-processed in `org-ql--normalize-query'.
;; The underscore before `on' prevents "unused lexical variable" warnings due to the
;; pre-processing converting that argument to FROM and TO. The `regexp' argument is
;; also provided by the pre-processing and is not to be given by the user. FROM and
;; TO are actually expected to be `ts' structs. The docstring is written for users.
"Return non-nil if current entry has a timestamp in given period.
If no arguments are specified, return non-nil if entry has any
timestamp.
If FROM, return non-nil if entry has a timestamp on or after
FROM.
If TO, return non-nil if entry has a timestamp on or before TO.
If ON, return non-nil if entry has a timestamp on date ON.
FROM, TO, and ON should be either `ts' structs, or strings
parseable by `parse-time-string' which may omit the time value.
TYPE may be `active' to match active timestamps, `inactive' to
match inactive ones, or `both' / nil to match both types.
LIMIT bounds the search for the timestamp REGEXP. It defaults to
the end of the entry, i.e. the position returned by
`org-entry-end-position', but for certain searches it should be
bound to a different positiion, e.g. for planning lines, the end
of the line after the heading."
;; MAYBE: Define active/inactive ones separately?
:normalizers ((`(,(or 'ts-active 'ts-a) . ,rest) `(ts :type active ,@rest))
(`(,(or 'ts-inactive 'ts-i) . ,rest) `(ts :type inactive ,@rest)))
:preambles ((`(,predicate-names . ,rest)
(list :regexp (pcase (plist-get rest :type)
((or 'nil 'both) org-tsr-regexp-both)
('active org-tsr-regexp)
('inactive org-ql-tsr-regexp-inactive))
;; Predicate needs testing only when args are present.
:query (-let (((&keys :from :to :on) rest))
;; FIXME: This used to be (when (or from to on) query), but that doesn't seem right, so I
;; changed it to this if, and the tests pass either way. Might deserve a little scrutiny.
(if (or from to on)
query
t)))))
;; TODO: DRY this with the clocked predicate.
:body
(cl-macrolet ((next-timestamp ()
`(when (re-search-forward regexp limit t)
(ts-parse-org (match-string match-group))))
(test-timestamps (pred-form)
`(cl-loop for next-ts = (next-timestamp)
while next-ts
thereis ,pred-form)))
(save-excursion
(cond ((not (or from to)) (re-search-forward regexp limit t))
((and from to) (test-timestamps (ts-in from to next-ts)))
(from (test-timestamps (ts<= from next-ts)))
(to (test-timestamps (ts<= next-ts to)))))))
(setf org-ql-cache (make-hash-table :weakness 'key))
(org-ql-select (org-agenda-files)
'(ts :from "2020-01-01" :to "2020-12-31")
:action '(substring-no-properties (org-get-heading t t)))))
("optimized"
(progn
(org-ql-defpred (ts ts-active ts-a ts-inactive ts-i)
(&key from to _on regexp (match-group 0) (limit (org-entry-end-position)))
;; NOTE: Arguments to this predicate are pre-processed in `org-ql--normalize-query'.
;; The underscore before `on' prevents "unused lexical variable" warnings due to the
;; pre-processing converting that argument to FROM and TO. The `regexp' argument is
;; also provided by the pre-processing and is not to be given by the user. FROM and
;; TO are actually expected to be `ts' structs. The docstring is written for users.
"Return non-nil if current entry has a timestamp in given period.
If no arguments are specified, return non-nil if entry has any
timestamp.
If FROM, return non-nil if entry has a timestamp on or after
FROM.
If TO, return non-nil if entry has a timestamp on or before TO.
If ON, return non-nil if entry has a timestamp on date ON.
FROM, TO, and ON should be either `ts' structs, or strings
parseable by `parse-time-string' which may omit the time value.
TYPE may be `active' to match active timestamps, `inactive' to
match inactive ones, or `both' / nil to match both types.
LIMIT bounds the search for the timestamp REGEXP. It defaults to
the end of the entry, i.e. the position returned by
`org-entry-end-position', but for certain searches it should be
bound to a different positiion, e.g. for planning lines, the end
of the line after the heading."
;; MAYBE: Define active/inactive ones separately?
:normalizers
((`(,(or 'ts-active 'ts-a) . ,rest) `(ts :type active ,@rest))
(`(,(or 'ts-inactive 'ts-i) . ,rest) `(ts :type inactive ,@rest)))
:preambles
((`(,predicate-names . ,(and rest (guard (or (plist-get rest :from)
(plist-get rest :to)
(plist-get rest :on)))))
(-let (((&plist :from :to :on :type) rest))
(org-ql--from-to-on)
(list :regexp (-let* ((from (or from (ts-adjust 'day (- org-ql-ts-days-from-default) (ts-now))))
(to (or to (ts-adjust 'day org-ql-ts-days-to-default (ts-now)))))
(org-ql--ts-range-to-regexp from to))
:query query)))
(`(,predicate-names . ,rest)
(list :regexp (pcase (plist-get rest :type)
((or 'nil 'both) org-tsr-regexp-both)
('active org-tsr-regexp)
('inactive org-ql-tsr-regexp-inactive))
;; Predicate needs testing only when args are present.
:query (-let (((&keys :from :to :on) rest))
;; FIXME: This used to be (when (or from to on) query), but that doesn't seem right, so I
;; changed it to this if, and the tests pass either way. Might deserve a little scrutiny.
(if (or from to on)
query
t)))))
;; TODO: DRY this with the clocked predicate.
:body
(cl-macrolet ((next-timestamp ()
`(when (re-search-forward regexp limit t)
(ts-parse-org (match-string match-group))))
(test-timestamps (pred-form)
`(cl-loop for next-ts = (next-timestamp)
while next-ts
thereis ,pred-form)))
(save-excursion
(cond ((not (or from to)) (re-search-forward regexp limit t))
((and from to) (test-timestamps (ts-in from to next-ts)))
(from (test-timestamps (ts<= from next-ts)))
(to (test-timestamps (ts<= next-ts to)))))))
(setf org-ql-cache (make-hash-table :weakness 'key))
(org-ql-select (org-agenda-files)
'(ts :from "2020-01-01" :to "2020-12-31")
:action '(substring-no-properties (org-get-heading t t)))))))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| optimized | 3.78 | 1.243185 | 0 | 0 |
| unoptimized | slowest | 4.700824 | 0 | 0 |
PROJECT Multi-pass query normalization
It would allow, e.g. one query to be normalized into another, and then into another. It would be helpful for timestamp-related ones, I think.
PROJECT [#B] Optimization for olp predicate
I noticed that, of course, a search like h:bar olp:foo is much faster than olp:foo,bar. Then I realized that the last argument to olp could be removed and replaced with a heading predicate with that argument, which is much faster.
[2020-12-05 Sat 01:27] However, this does not always work correctly, as yantar92 reported:
Consider the following example:
- No deadline
** Learn *** Research **** Plasticity ***** TODO Schwaiger [MRS-Fall] (2017) Characterizing the mechanical properties of individual phases in nanostructured composites
I may try to match the last heading like the following “olp:dead phase”. It will not match.
Devise solution to desired olp optimization
NEXT Write test for olp that catches bug that attempted optimization caused
PROJECT [#B] Outline path predicate
[2019-10-07 Mon 11:15] There are two potential types of matching on outline paths: matching on any part of the outline path, and matching a specific path. For example, with this file:
* Food
** Fruits
*** Blueberries
*** Grapes
** Vegetables
*** Carrots
*** Potatoes
Matching could work like this:
(outline "Food")- Would return all nodes.
(outline "Fruits")- Would return all fruits.
Matching at a specific path would be something like:
(outline-path "Food" "Fruits")- Would return all fruits. But if there were another
Fruitsheading somewhere in the file, under a different outline path, it would not return its nodes.
I’m not sure the second type of matching belongs in predicates, but rather in this.
To implement this with good performance probably needs an outline-path cache. I can probably repurpose the tags caching, but maybe it should be generalized.
[2019-10-07 Mon 13:09] This is basically done with be2bf6df316b96b3ed56851b8ffe0e227796b621 and be2bf6df316b96b3ed56851b8ffe0e227796b621, but not the specific-path matching. I left a MAYBE in the code about “anchored” path matching, which would accomplish that.
PROJECT Tools for saving queries and accessing them [3/4]
- Added example to
examples.org.
PROJECT [#A] Org link types [2/3]
This would be useful for having a menu of saved queries as Org links, or even bookmarking saved queries.
For saved queries
For searches
[2020-11-08 Sun 22:59] Let’s try a very simple implementation so I could write a link like this to search the current buffer:
[[org-ql-search:property:author="AUTHOR"]]
[2020-11-08 Sun 23:22] Seems to work!
;;;; Org link type
;; This section adds a custom link type to Org. See info:org#Adding hyperlink types.
(org-link-set-parameters "org-ql-search"
:follow #'org-ql-search--link-open
:store #'org-ql-search--link-store)
(defun org-ql-search--link-open (query)
"Open Org QL QUERY for current buffer."
(org-ql-search (current-buffer) query))
(defun org-ql-search--link-store ()
"Store a link to current Org QL query."
;; TODO: When we have an org-ql-view-mode, test it here instead of org-ql-view-query.
(when org-ql-view-query
(org-store-link-props :type "org-ql-search"
:link (concat "org-ql-search:" (org-ql-view--format-query org-ql-view-query))
:description org-ql-view-title)
t))Tested on these queries:
+ [[org-ql-search:(property%20:author%20"Chris%20Wellons")][org-ql-search:(property :author "Chris Wellons")]]
+ [[org-ql-search:(link%20"nullprogram")][org-ql-search:(link "nullprogram")]]
+ [[org-ql-search:link:nullprogram]]
[2020-11-10 Tue 00:35] I’d like to support other parameters to the search, like grouping and sorting, so:
;;;; Org link type
;; This section adds a custom link type to Org. See info:org#Adding hyperlink types.
(org-link-set-parameters "org-ql-search"
:follow #'org-ql-search--link-open
:store #'org-ql-search--link-store)
(defun org-ql-search--link-open (query)
"Open Org QL QUERY for current buffer."
(require 'url-parse)
(pcase-let* ((`(,query . ,params)
(url-path-and-query (url-parse-make-urlobj "org-ql-search" nil nil nil nil
query)))
(params (url-parse-query-string params))
;; Hacky or elegant?
(_ (mapc (lambda (pair)
(cl-callf (lambda (it)
(intern (concat ":" it)))
(car pair))
(cl-callf read (cdr pair)))
params))
(params (cl-loop for (key . value) in params
append (list key value))))
(apply #'org-ql-search (current-buffer) query params)))
(defun org-ql-search--link-store ()
"Store a link to current Org QL query."
(when org-ql-view-query
(org-store-link-props :type "org-ql-search"
:link (concat "org-ql-search:" (org-ql-view--format-query org-ql-view-query))
:description org-ql-view-title)
t))That seems to work, like:
[[org-ql-search:property:author="Chris%20Wellons"?super-groups=((:auto-outline-path%20t))]]
[2020-11-10 Tue 01:34] Okay, this seems to take care of all parameters:
(defun org-ql-search--link-open (path)
"Open Org QL query for current buffer at PATH.
PATH should be the part of an \"org-ql-search:\" URL after the
protocol. See, e.g. `org-ql-search--link-store'."
(require 'url-parse)
(require 'url-util)
(pcase-let* ((`(,query . ,params) (url-path-and-query
(url-parse-make-urlobj "org-ql-search" nil nil nil nil
path)))
(query (url-unhex-string query))
(params (when params
(url-parse-query-string params)))
;; `url-parse-query-string' returns "improper" alists, which makes this awkward.
(sort (when (alist-get "sort" params nil nil #'string=)
(read (alist-get "sort" params nil nil #'string=))))
(groups (when (alist-get "super-groups" params nil nil #'string=)
(read (alist-get "super-groups" params nil nil #'string=))))
(title (when (alist-get "title" params nil nil #'string=)
(read (alist-get "title" params nil nil #'string=)))))
(org-ql-search (current-buffer) query
:sort sort
:super-groups groups
:title title)))
(defun org-ql-search--link-store ()
"Store a link to the current Org QL view.
Only views that search a single buffer may be linked to."
(require 'url-parse)
(require 'url-util)
(unless (or (bufferp org-ql-view-buffers-files) (= 1 (length org-ql-view-buffers-files)))
(user-error "Only views searching a single buffer may be linked"))
(when org-ql-view-query
(let* ((params (list (when org-ql-view-super-groups
(list "super-groups" (prin1-to-string org-ql-view-super-groups)))
(when org-ql-view-sort
(list "sort" (prin1-to-string org-ql-view-sort)))
(when org-ql-view-title
(list "title" (prin1-to-string org-ql-view-title)))))
(filename (concat (url-hexify-string (org-ql-view--format-query org-ql-view-query))
"?" (url-build-query-string (delete nil params))))
(url (url-recreate-url (url-parse-make-urlobj "org-ql-search" nil nil nil nil
filename))))
(org-store-link-props
:type "org-ql-search"
:link url
:description (concat "org-ql-search: " org-ql-view-title)))
t))For all parameters
Bookmarks
[2020-11-08 Sun 23:25] Already done in e5b4cd106558790563af26a8e32ec9508f904855.
Access saved query from saved query list
Save query from ql-agenda buffer
PROJECT [#B] Group tag support
- Tag hierarchy support · Issue #145 · alphapapa/org-ql · GitHub
- Change: (org-ql–tags-at) Support tag hierarchies by maxchaos · Pull Request #146 · alphapapa/org-ql · GitHub
UNDERWAY Benchmarking tags searches without and with new group-tags support
(bench-multi-lexical :times 10 :ensure-equal t
:forms (("without group-tags support"
(org-ql-select (org-ql-search-directories-files)
'(tags "Emacs")
:action #'point))
))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| without group-tags support | slowest | 5.512271 | 0 | 0 |
(bench-multi-lexical :times 10 :ensure-equal t
:forms (("with group-tags support"
(org-ql-select (org-ql-search-directories-files)
'(tags "Emacs")
:action #'point))
))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| with group-tags support | slowest | 5.154639 | 0 | 0 |
[2020-11-09 Mon 17:43] I think I need to enhance the benchmarking macros to make this easier. But that might require copying much of benchmark-run-compiled, so let me try something else: This is messy, but it ought to be fair enough (the only difference being the minor change in org-ql--tags-at.
(bench-multi-lexical :times 10 :ensure-equal t
:forms (("without group-tags support"
(progn
(setf org-ql-cache (make-hash-table :weakness 'key)
org-ql-tags-cache (make-hash-table :weakness 'key)
org-ql-node-value-cache (make-hash-table :weakness 'key))
(defun org-ql--expand-tag-hierarchy (tags &optional excluded)
"Return TAGS along with their associated group tags.
This function recursively searches for groups that each given tag belongs to,
directly or indirectly, and includes the corresponding group tags to the result.
TAGS should be a list of tags (i.e., strings).
If non-nil, EXCLUDED should be a list of group tags that will not be
automatically added to the results unless they are already in TAGS."
(let ((groups (org-tag-alist-to-groups org-current-tag-alist))
(excluded (append tags excluded)))
(let (group-tags)
(dolist (tag tags)
(pcase-dolist (`(,group-tag . ,group-members) groups)
(when (and (not (member group-tag excluded))
;; Check if one of the members in the group matches tag.
;; Notice that each member may be a plain string or
;; a regexp pattern (enclosed between curly brackets).
(--some (if (string-match-p "^[{].+[}]$" it)
;; If pattern (it) is a regexp, remove the brackets and
;; make sure that it either matches the whole tag or not.
(string-match-p (concat "^" (substring it 1 -1) "$") tag)
;; Check if member (it) is identical to tag.
(string= it tag))
group-members))
(push group-tag group-tags))))
;; If group tags not already included have been found,
;; then recursively expand them as well.
;; Notice that by passing (group-tags excluded) to the next call
;; instead of ((append tags group-tags)) ensures that we do not
;; unnecessarily loop over the elements of TAGS more than once.
(if group-tags
(append tags (org-ql--expand-tag-hierarchy group-tags excluded))
tags))))
(defun org-ql--tags-at (position)
"Return tags for POSITION in current buffer.
Returns cons (INHERITED-TAGS . LOCAL-TAGS)."
;; I'd like to use `-if-let*', but it doesn't leave non-nil variables
;; bound in the else clause, so destructured variables that are non-nil,
;; like found caches, are not available in the else clause.
(if-let* ((buffer-cache (gethash (current-buffer) org-ql-tags-cache))
(modified-tick (car buffer-cache))
(tags-cache (cdr buffer-cache))
(buffer-unmodified-p (eq (buffer-modified-tick) modified-tick))
(cached-result (gethash position tags-cache)))
;; Found in cache: return them.
(pcase cached-result
('org-ql-nil nil)
(_ cached-result))
;; Not found in cache: get tags and cache them.
(let* ((local-tags (or (when (looking-at org-ql-tag-line-re)
(split-string (match-string-no-properties 2) ":" t))
'org-ql-nil))
(inherited-tags (or (when org-use-tag-inheritance
(save-excursion
(if (org-up-heading-safe)
;; Return parent heading's tags.
(-let* (((inherited local) (org-ql--tags-at (point)))
(tags (when (or inherited local)
(cond ((and (listp inherited)
(listp local))
(->> (append inherited local)
-non-nil -uniq))
((listp inherited) inherited)
((listp local) local)))))
(cl-typecase org-use-tag-inheritance
(list (setf tags (-intersection tags org-use-tag-inheritance)))
(string (setf tags (--select (string-match org-use-tag-inheritance it)
tags))))
(pcase org-tags-exclude-from-inheritance
('nil tags)
(_ (-difference tags org-tags-exclude-from-inheritance))))
;; Top-level heading: use file tags.
org-file-tags)))
'org-ql-nil))
(all-tags (list inherited-tags local-tags)))
;; Check caches again, because they may have been set now.
;; TODO: Is there a clever way we could avoid doing this, or is it inherently necessary?
(setf buffer-cache (gethash (current-buffer) org-ql-tags-cache)
modified-tick (car buffer-cache)
tags-cache (cdr buffer-cache)
buffer-unmodified-p (eq (buffer-modified-tick) modified-tick))
(unless (and buffer-cache buffer-unmodified-p)
;; Buffer-local tags cache empty or invalid: make new one.
(setf tags-cache (make-hash-table))
(puthash (current-buffer)
(cons (buffer-modified-tick) tags-cache)
org-ql-tags-cache))
(puthash position all-tags tags-cache))))
(org-ql-select (org-ql-search-directories-files)
'(tags "Emacs")
:action #'point)))
("with group-tags support"
(progn
(setf org-ql-cache (make-hash-table :weakness 'key)
org-ql-tags-cache (make-hash-table :weakness 'key)
org-ql-node-value-cache (make-hash-table :weakness 'key))
(defun org-ql--expand-tag-hierarchy (tags &optional excluded)
"Return TAGS along with their associated group tags.
This function recursively searches for groups that each given tag belongs to,
directly or indirectly, and includes the corresponding group tags to the result.
TAGS should be a list of tags (i.e., strings).
If non-nil, EXCLUDED should be a list of group tags that will not be
automatically added to the results unless they are already in TAGS."
(let ((groups (org-tag-alist-to-groups org-current-tag-alist))
(excluded (append tags excluded)))
(let (group-tags)
(dolist (tag tags)
(pcase-dolist (`(,group-tag . ,group-members) groups)
(when (and (not (member group-tag excluded))
;; Check if one of the members in the group matches tag.
;; Notice that each member may be a plain string or
;; a regexp pattern (enclosed between curly brackets).
(--some (if (string-match-p "^[{].+[}]$" it)
;; If pattern (it) is a regexp, remove the brackets and
;; make sure that it either matches the whole tag or not.
(string-match-p (concat "^" (substring it 1 -1) "$") tag)
;; Check if member (it) is identical to tag.
(string= it tag))
group-members))
(push group-tag group-tags))))
;; If group tags not already included have been found,
;; then recursively expand them as well.
;; Notice that by passing (group-tags excluded) to the next call
;; instead of ((append tags group-tags)) ensures that we do not
;; unnecessarily loop over the elements of TAGS more than once.
(if group-tags
(append tags (org-ql--expand-tag-hierarchy group-tags excluded))
tags))))
(defun org-ql--tags-at (position)
"Return tags for POSITION in current buffer.
Returns cons (INHERITED-TAGS . LOCAL-TAGS)."
;; I'd like to use `-if-let*', but it doesn't leave non-nil variables
;; bound in the else clause, so destructured variables that are non-nil,
;; like found caches, are not available in the else clause.
(if-let* ((buffer-cache (gethash (current-buffer) org-ql-tags-cache))
(modified-tick (car buffer-cache))
(tags-cache (cdr buffer-cache))
(buffer-unmodified-p (eq (buffer-modified-tick) modified-tick))
(cached-result (gethash position tags-cache)))
;; Found in cache: return them.
(pcase cached-result
('org-ql-nil nil)
(_ cached-result))
;; Not found in cache: get tags and cache them.
(let* ((local-tags (or (when (looking-at org-ql-tag-line-re)
(split-string (match-string-no-properties 2) ":" t))
'org-ql-nil))
(inherited-tags (or (when org-use-tag-inheritance
(save-excursion
(if (org-up-heading-safe)
;; Return parent heading's tags.
(-let* (((inherited local) (org-ql--tags-at (point)))
(tags (when (or inherited local)
(cond ((and (listp inherited)
(listp local))
(->> (append inherited local)
-non-nil -uniq))
((listp inherited) inherited)
((listp local) local)))))
(cl-typecase org-use-tag-inheritance
(list (setf tags (-intersection tags org-use-tag-inheritance)))
(string (setf tags (--select (string-match org-use-tag-inheritance it)
tags))))
(pcase org-tags-exclude-from-inheritance
('nil tags)
(_ (-difference tags org-tags-exclude-from-inheritance))))
;; Top-level heading: use file tags.
org-file-tags)))
'org-ql-nil))
all-tags)
(when org-group-tags
(unless (eq local-tags 'org-ql-nil)
(setq local-tags (org-ql--expand-tag-hierarchy local-tags)))
(unless (eq inherited-tags 'org-ql-nil)
(setq inherited-tags (org-ql--expand-tag-hierarchy inherited-tags))))
(setq all-tags (list inherited-tags local-tags))
;; Check caches again, because they may have been set now.
;; TODO: Is there a clever way we could avoid doing this, or is it inherently necessary?
(setf buffer-cache (gethash (current-buffer) org-ql-tags-cache)
modified-tick (car buffer-cache)
tags-cache (cdr buffer-cache)
buffer-unmodified-p (eq (buffer-modified-tick) modified-tick))
(unless (and buffer-cache buffer-unmodified-p)
;; Buffer-local tags cache empty or invalid: make new one.
(setf tags-cache (make-hash-table))
(puthash (current-buffer)
(cons (buffer-modified-tick) tags-cache)
org-ql-tags-cache))
(puthash position all-tags tags-cache))))
(org-ql-select (org-ql-search-directories-files)
'(tags "Emacs")
:action #'point)))
))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| without group-tags support | 1.01 | 52.832562 | 4 | 1.989522 |
| with group-tags support | slowest | 53.425342 | 5 | 2.479128 |
[2020-11-09 Mon 17:57] Well, the performance difference seems smaller than I expected. For single iterations, it ought to be unnoticeable. Although I’m still a bit skeptical about this benchmark: I feel like it ought to have more of an impact than that, but maybe I’m wrong–and that would be great!
Next steps:
- [X] Post benchmark code on PR and ask Panagiotis to verify
- [X] Also ask him to run benchmark actually using group tags (since I don’t actually have any, even though the boolean is t)
- [X] Discuss caching of group tag expansion. It seems like we ought to cache the expansions as well, because sibling headings (especially at level 1) ought to get their group tags re-expanded individually, even when we’ve already expanded them for another heading.
- [X] Remove unused
resultvariable
PROJECT [#B] Document the sorting functions
Note that the built-in sorting only works on Org elements, which is the default :action. So if a different action is used, sorting will not work. In that case, the action should be mapped across the Org element results from outside the org-ql form.
PROJECT [#B] Recursive queries
For lack of a better term. A way to query for certain headings, and then gather results of a different query at each result of the first query, displaying all results in a single view.
This works pretty well. It needs polishing, and some refactoring so items can be indented completely (rather than leaving the keyword unindented, as it is now).
(cl-defun org-ql-agenda-recursive (buffers-or-files queries &key action narrow sort)
(cl-labels ((rec (queries element indent)
(org-with-point-at (org-element-property :org-marker element)
(when-let* ((results (progn
(org-narrow-to-subtree)
(org-ql-select (current-buffer)
(car queries)
:action 'element-with-markers
:narrow t
:sort sort))))
;; Indent entry for each level
(setf results (--map
(org-element-put-property it :raw-value
(concat (s-repeat (* 5 indent) " ")
(org-element-property :raw-value it)))
results))
(cons it (if (cdr queries)
(--map (rec (cdr queries) it)
results)
results))))))
(when-let* ((indent 0)
(results (org-ql-select buffers-or-files
(car queries)
:action 'element-with-markers
:narrow narrow
:sort sort)))
(->> (if (cdr queries)
(--map (rec (cdr queries) it (1+ indent))
results)
results)
(-flatten-n (1- (length queries)))
-non-nil
(org-ql-agenda--agenda nil nil
:entries)))))
(cl-defun org-ql-select-recursive (buffers-or-files queries &key action narrow sort)
(cl-labels ((rec (queries element indent)
(org-with-point-at (org-element-property :org-marker element)
(when-let* ((results (progn
(org-narrow-to-subtree)
(org-ql-select (current-buffer)
(car queries)
:action 'element-with-markers
:narrow t
:sort sort))))
;; Indent entry for each level
(setf results (--map
(org-element-put-property it :raw-value
(concat (s-repeat (* 5 indent) " ")
(org-element-property :raw-value it)))
results))
(cons it (if (cdr queries)
(--map (rec (cdr queries) it)
results)
results))))))
(when-let* ((indent 0)
(results (org-ql-select buffers-or-files
(car queries)
:action 'element-with-markers
:narrow narrow
:sort sort)))
(->> (if (cdr queries)
(--map (rec (cdr queries) it (1+ indent))
results)
results)
(-flatten-n (1- (length queries)))
-non-nil))))PROJECT [#B] Timeline view
e.g. as mentioned by Samuel Wales at https://lists.gnu.org/archive/html/emacs-orgmode/2019-08/msg00330.html. Prototype code:
(cl-defun org-ql-timeline (buffers-files query)
(let ((results
(org-ql-select buffers-files
query :action
(lambda ()
(let* ((heading-string
(->> (org-element-headline-parser
(line-end-position))
org-ql--add-markers
org-ql-agenda--format-element))
(timestamps
(cl-loop with limit = (org-entry-end-position)
while (re-search-forward org-ts-regexp-both
limit t)
collect (ts-parse-org (match-string 0))))
(timestamp-strings
(->> timestamps
(-sort #'ts<)
(--map (concat " " (ts-format it))))))
(s-join "\n" (cons heading-string timestamp-strings))))
:sort '(date))))
(org-ql-agenda--agenda nil nil :strings results)))
(org-ql-timeline (org-agenda-files)
'(and "Emacs" (ts)))
;; More timeline-like version, organized by date rather than task.
(cl-defun org-ql-timeline* (buffers-files query &key filter-ts)
(let* ((ts-ht (ht))
(results (org-ql-select buffers-files
query
:action (lambda ()
(let* ((heading-string
(->> (org-element-headline-parser
(line-end-position))
org-ql--add-markers
org-ql-agenda--format-element))
(date-timestamps
;; Each one set to 00:00:00.
(cl-loop with limit = (org-entry-end-position)
while (re-search-forward org-ts-regexp-both
limit t)
collect (->> (match-string 0)
ts-parse-org
(ts-apply :hour 0 :minute 0 :second 0)))))
(setf date-timestamps (delete-dups date-timestamps))
(when filter-ts
(setf date-timestamps (cl-remove-if-not filter-ts date-timestamps)))
(--each date-timestamps
(push heading-string (gethash it ts-ht)))))))
(tss-sorted (-sort #'ts< (ht-keys ts-ht)))
(strings (cl-loop for ts in tss-sorted
collect (concat "\n"
(propertize (ts-format "%Y-%m-%d" ts)
'face 'org-agenda-structure))
append (ht-get ts-ht ts))))
(org-ql-agenda--agenda nil nil :strings strings)))
(org-ql-timeline* (org-agenda-files)
'(ts :from -14)
:filter-ts `(lambda (ts)
(ts<= ,(ts-adjust 'day -14 (ts-now)) ts)))Another, more up-to-date implementation:
;; NOTE: ts structs don't (sometimes? or always?) compare properly
;; with default hash tables, e.g. this code:
;; (let* ((ts-a #s(ts nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil 1572670800.0))
;; (ts-b #s(ts nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil 1572584400.0)))
;; (list :equal (equal ts-a ts-b)
;; :sxhash-equal (equal (sxhash ts-a) (sxhash ts-b)))) ;;=> (:equal nil :sxhash-equal t)
;; So we must use the "contents-hash" table as described in the Elisp manual.
(define-hash-table-test 'contents-hash 'equal 'sxhash-equal)
(cl-defun org-ql-view-timeline (buffers-files &key from to on)
"FIXME: DOcstring"
(cl-flet ((parse-ts-arg
(arg type)
;; Parse ARG as a string or TS struct and adjust it to the beginning
;; or end of its day, depending on whether TYPE is `:begin' or `:end'.
(-let (((hour minute second) (cl-ecase type
(:begin '(0 0 0))
(:end '(23 59 59)))))
(->> (cl-typecase arg
(string (ts-parse arg))
(ts arg))
(ts-apply :hour hour :minute minute :second second)))))
(let* ((ts-predicate `(lambda (ts)
,(cond (on `(ts-in ,(parse-ts-arg on :begin)
ts
,(parse-ts-arg on :end)))
((and from to) `(ts-in ,(parse-ts-arg from :begin)
ts
,(parse-ts-arg to :end)))
(from `(ts<= ,(parse-ts-arg from :begin) ts))
(to `(ts<= ts ,(parse-ts-arg to :end)))
(t (user-error "Huh?")))))
(query (cond (on `(ts :from ,(parse-ts-arg on :begin)
:to ,(parse-ts-arg on :end)))
(t (append (list 'ts)
(when from
`(:from ,(parse-ts-arg from :begin)))
(when to
`(:to ,(parse-ts-arg to :end)))))))
(date-ts-table (make-hash-table :test 'contents-hash))
(_results (org-ql-select buffers-files query
:action (lambda ()
(let* ((string (->> (org-element-headline-parser
(line-end-position))
org-ql--add-markers
org-ql-view--format-element)))
(cl-loop with limit = (org-entry-end-position)
while (re-search-forward org-ts-regexp-both limit t)
for ts = (->> (match-string 0) ts-parse-org)
when (funcall ts-predicate ts)
do (cl-pushnew (cons ts (concat (ts-format " %H:%M" ts)
string))
(gethash (ts-apply :hour 0 :minute 0 :second 0 ts)
date-ts-table)
:test #'equal))))))
(date-tss-sorted (->> date-ts-table hash-table-keys (-sort #'ts<)))
(string (cl-loop for date-ts in date-tss-sorted
for date-string = (propertize (ts-format "%Y-%m-%d" date-ts)
'face 'org-agenda-structure)
concat (concat "\n" date-string)
concat (cl-loop for (ts . entry) in (->> (gethash date-ts date-ts-table)
(-sort (-on #'ts< #'car)))
concat (concat "\n" entry)))))
(org-ql-view--display :buffer "Timeline"
:header "Timeline"
:string string))))
;; Used like:
;; (org-ql-view-timeline "~/org/main.org" :from "2019-11-01")[2019-09-26 Thu 21:28] Would probably make sense to implement this using the view-sections someday.
PROJECT [#B] Implement view with tabulated-list-mode or magit-section
[2019-09-02 Mon 05:20] Especially with some of the new packages that make tabulated-list-mode easier to use, like navigel. However, it would probably break grouping, or require some kind of adapter or extension to do grouping, so I don’t know if that would work. Something like magit-section would be more flexible, and could be recursively grouped, like in magit-todos.
[2019-09-08 Sun 10:06] Came up with a prototype yesterday, in branch wip/view-section. Seems to work pretty well.
One of the things in that branch is org-ql-item, which is a struct used to carry data for query results. It seems to work well.
Another idea for it is to simply store the element from org-element-headline-parser in one of its slots, and populate all of the other slots lazily, like ts. It already does that for a couple of slots, but I think it makes sense to do it for all of them, to reduce the overhead of making the struct for every query result.
MAYBE [#C] Experiment with widget
The code that powers the customization UI. Has collapsible and customizable widgets. Might be perfect. Might even enable editing items in the list, with functions to make the changes in the source buffers.
Code idea
Inserting items into a view could look something like this:
(org-ql-view--insert-items
:header (ts-format "%Y-%m-%d" (ts-now))
:items (org-ql-query
:select #'org-ql-current-item
:from (org-agenda-files)
:where '(or (deadline auto)
(scheduled :on today)
(ts-active :on today)))
:group-by '(org-ql-item-priority
org-ql-item-todo))Items would be structs, and the group-by argument would be a list of accessors, like how magit-todos works. Arbitrary functions could also be passed to group-by, as whatever value the function returns is used to group them. org-ql-current-item would be a function that turns the result of org-element-headline-parser into the struct.
Not sure if it should automatically add the number of items to the header, or if that should be done manually.
Prior art
GitHub - m2ym/direx-el: Directory Explorer for GNU Emacs
Appears to be another implementation of magit-section-like expandable sections. Not sure which came first. Its code seems like it may be helpful.
magit-section
PROJECT Dynamic blocks
PROJECT [#B] Save view to dynamic block
[2020-11-10 Tue 04:31] A command would save users from having to write out the dynamic block manually.
[2020-11-12 Thu 03:23] A command to do this would be very helpful. (Yes, I entered this idea twice. I should use my own systems better, apparently. But that’s what this package is all about, right?)
NEXT Lookup Org function to create dynamic blocks
NEXT Write function to save view to dynamic block
[#A] Implement dynamic blocks
- Tasks
- [X] Merge code
- [X] Document the feature
For example, this blog article shows a way that Org’s existing dynamic columnview blocks can be very useful. org-ql queries could be useful in them as well.
[2020-11-09 Mon 22:00] I just realized that this is probably much easier than I realized.
- info:org#Dynamic blocks
(cl-defun org-dblock-write:org-ql (params)
"FIXME: Docstring"
(pcase-let* (((map :query :columns :sort :ts-format) params)
(format-fns (list (cons 'heading (lambda (element)
(org-make-link-string (org-element-property :raw-value element)
(org-element-property :raw-value element))))
(cons 'todo (lambda (element)
(or (org-element-property :todo-keyword element)
"")))
(cons 'priority (lambda (element)
(--if-let (org-element-property :priority element)
(char-to-string it)
"")))
(cons 'deadline (lambda (element)
(--if-let (org-element-property :deadline element)
(ts-format ts-format (ts-parse-org-element it ))
"")))
(cons 'scheduled (lambda (element)
(--if-let (org-element-property :scheduled element)
(ts-format ts-format (ts-parse-org-element it ))
"")))))
(elements (org-ql-query :from (current-buffer)
:where query
:select '(org-element-headline-parser (line-end-position))
:order-by sort)))
(cl-labels ((format-element
(element) (string-join (cl-loop for column in columns
for fn = (alist-get column format-fns)
collect (funcall fn element))
" | ")))
(insert "| " (string-join (--map (capitalize (symbol-name it)) columns) " | ") " |" "\n")
(insert "|- \n")
(dolist (element elements)
(insert "| " (format-element element) " |" "\n"))
(org-table-align))))[2020-11-09 Mon 22:35] This works pretty well! For example:
#+BEGIN: org-ql :query (todo) :format (priority todo heading deadline scheduled) :sort (priority date) :ts-format "%Y-%m-%d %H:%M"
| Priority | Todo | Heading | Deadline | Scheduled |
|----------+-------+------------+------------------+------------------|
| A | TODAY | Heading 1 | 2020-11-11 00:00 | |
| B | TODO | Heading 2 | | 2020-11-09 00:00 |
#+END:
#+BEGIN: columnview :id global :hlines t :indent t
| ITEM | TODO | PRIORITY | TAGS |
|----------------+-------+----------+------|
| Test heading 1 | TODAY | B | |
|----------------+-------+----------+------|
| Heading 2 | TODO | B | |
#+END:
* TODAY [#A] Heading 1
DEADLINE: <2020-11-11 Wed>
* TODO [#B] Heading 2
SCHEDULED: <2020-11-09 Mon>
[2020-11-10 Tue 00:03] I think it’s ready to merge now.
[#A] Org block to insert results of queries with links to entries
[2020-01-16 Thu 06:20] This idea just came to me when I was thinking about using the search-based paradigm vs. outline-based. This would allow both, e.g. some kind of #+BEGIN_QUERY block that would update when C-c C-c is pressed on it.
[2020-11-13 Fri 22:57] I keep rediscovering ideas that I’ve had previously. This is now done as the dynamic block feature. I guess I should actually use these tools I’ve made.
PROJECT [#B] Predicate helper functions
[2020-11-24 Tue 16:50] This idea started off by writing a week predicate:
(org-ql-defpred week (&optional relative)
"Match entries with a timestamp in the calendar week RELATIVE.
RELATIVE is a number relative to the current week (i.e. 0 or nil
is this week, -1 is last week, 1 is next week)."
:normalizers ((`(,predicate-names . ,(or `(,(and (pred numberp) relative)) `nil))
(let* ((relative (or relative 0))
(now (ts-now))
(week-start (->> now
(ts-adjust 'day (- 0 (ts-dow now)))
(ts-adjust 'day (* 7 relative))
(ts-apply :hour 0 :minute 0 :second 0)))
(week-end (->> now
(ts-adjust 'day (- 7 (ts-dow now)))
(ts-adjust 'day (* 7 relative))
(ts-apply :hour 23 :minute 59 :second 59))))
`(ts :from ,week-start :to ,week-end)))
(`(,predicate-names ,(and (pred stringp) date))
(let* ((then (ts-parse date))
(week-start (->> then
(ts-adjust 'day (- 0 (ts-dow then)))
(ts-apply :hour 0 :minute 0 :second 0)))
(week-end (->> then
(ts-adjust 'day (- 7 (ts-dow then)))
(ts-apply :hour 23 :minute 59 :second 59))))
`(ts :from ,week-start :to ,week-end)))))That works pretty well:
(list (org-ql--normalize-query '(week))
(org-ql--normalize-query '(week -1))
(org-ql--normalize-query '(week "2020-11-01")))
(Hmm, the patterns in those beginning-of-week and end-of-week timestamps are interesting…)
And that’s pretty useful. But what if someone wanted to write a query like (closed :on (week -1)) to match entries closed in the last week? It wouldn’t help at all.
One idea would be to add a type argument to the week predicate. But that would be awkward and a bit ugly. And it wouldn’t solve the problem, anyway, because while the ts predicate can take a type argument for active, inactive, or both, it can’t do, e.g. clocked, closed, deadline, etc.
So what’s really needed is a way to insert the week-based arguments into other selectors. But that would require there to be a (week) function defined, which would mean polluting the global namespace and potential conflicts.
So maybe another macro could define “helper” functions which would be available in the normalizers. Or maybe the defpred macro could take another argument for “helpers”, although that would probably require binding them around each pcase expression–not hard, but not especially elegant.
The next question is, how would those work in string queries? I’m not sure there’s a good way to translate them.
PROJECT [#C] New Transient transient-lisp-variable class
[2020-10-19 Mon 00:23] Should try to use this instead of whatever bespoke code is currently used.
PROJECT [#C] Normalize queries
[2019-07-16 Tue 11:49] This serves two purposes:
- Equivalent queries will return the same results from the cache.
- The selectors that can be converted to the fastest preamble regexps will be sorted first, so the fastest preamble will be used. Although this may not always be straightforward. For example, in a file with only a few
TODOitems, the(todo "TODO")selector would convert to a preamble that would quickly search through the file. But if there were a thousandTODOitems, it wouldn’t be as much of a benefit, and a(regexp "something")selector’s preamble might be much faster, depending on how many timessomethingappears in the file.
So the second purpose might actually be a drawback, because it would prevent users from optimizing their queries with knowledge of their data. Maybe there should be an option to not normalize queries, so advanced users can order their selectors manually.
PROJECT [#C] Update view screenshots
e.g. doesn’t currently show the View header.
PROJECT [#C] Test caching
See notes on 1dce9467f25428b5289d3665cd840820969ed65a. It would be good to test the caching explicitly, at least for some queries, because if I were to completely break it again, in such a way that results were stored but retrieval always failed, the tests wouldn’t catch it.
CANCELED [#C] Alternative parsing libraries
- e.g. Bovine and Wisent come with Emacs, which would allow us to drop the
pegdependency(which doesn’t use lexical binding) - parsec is third-party, but looks powerful
[2021-09-08 Wed 15:40] (peg does use lexical binding now, and it’s in ELPA and getting a bit of attention, so no need to drop it, I think.)
[#A] Timestamp predicates using relative dates break caching
Could fix this in a 0.5.3 release, but I think too much code changes are involved for a bugfix release.
Fix/commit checklists
- Closing issue: 223
Move from-to-on argument processing to query normalizers
Update defpred docs
- [X] Mention that normalizers are run until they return the same result; need to guard against infinite loops.
- [X] Update docstring
- [X] Update readme
- [X] defpred tutorial
Remove old --from-to-on macro completely
closed
- [X] Use new
--normalize-from-to-onmacro - [X] Remove old
--from-to-onfrom--query-predicate - [X] Add tests
- [X] Query normalization
- [X] Results
clocked
- [X] Use new
--normalize-from-to-onmacro - [X] Remove old
--from-to-onfrom--query-predicate - [X] Add tests
- [X] Query normalization
- [X] Results
ts
- [X] Use new
--normalize-from-to-onmacro - [X] Remove old
--from-to-onfrom--query-predicate - [X] Add tests
- [X] Query normalization
- [X] Results
planning
- [X] Use new
--normalize-from-to-onmacro - [X] Remove old
--from-to-onfrom--query-predicate - [X] Add tests
- [X] Query normalization
- [X] Results
deadline
- [X] Use new
--normalize-from-to-onmacro - [X] Remove old
--from-to-onfrom--query-predicate - [X] Add tests
- [X] Query normalization
- [X] Results
scheduled
- [X] Use new
--normalize-from-to-onmacro - [X] Remove old
--from-to-onfrom--query-predicate - [X] Add tests
- [X] Query normalization
- [X] Results
[#A] ~(link :target)~ doesn’t work
[#A] ~(link :regexp-p)~ doesn’t work
[#A] Fix: Custom sorter breaks cached results
- custom sort functions pollute cache · Issue #186 · alphapapa/org-ql · GitHub
- replace sort with -sort. by natask · Pull Request #187 · alphapapa/org-ql · GitHub
[#A] Update dash dependencies
- Out-of-date dependences (dash-functional) · Issue #209 · alphapapa/org-ql · GitHub
- Bump dash.el to fix helm-org-ql by landakram · Pull Request #197 · alphapapa/org-ql · GitHub
[#A] Checking links for unsafe parameters
- State “PROJECT” from “UNDERWAY” [2020-11-12 Thu 00:26]
- State “UNDERWAY” from [2020-11-11 Wed 23:09]
Theoretically one could put a sexp-based query into a link that would run arbitrary code to do something evil. Like:
org-ql-search:(message “AHA”)
That’s very unlikely to be abused, but it would be good to protect against it. Two possibilities:
- For sexp-based queries in links and dynamic blocks, prompt for confirmation before running.
- Use a special variable to control whether lambdas and arbitrary sexps are allowed in queries, and disable it for links and dynamic blocks. (That might be difficult to do, since they could be buried in an
andor something. A whitelist approach might be needed.)
[#A] Tag org-super-agenda 1.2 and bump required version in org-ql
That should force the version of org-super-agenda with the fix to be installed when org-ql is upgraded.
[2020-11-22 Sun 17:19] org-super-agenda 1.2 is tagged and released, so now we can depend on it.
Add automated tests
- State “UNDERWAY” from “TODO” [2020-11-12 Thu 00:24]
- State “TODO” from “MAYBE” [2020-11-11 Wed 23:16]
- State “MAYBE” from [2020-11-11 Wed 23:15]
Maybe impractical, but maybe we could at least test that potentially unsafe ones signal errors.
[2020-11-12 Thu 00:24] Works better than I expected. All the tests seem to correctly pass, signaling the correct errors for the correct reasons–except for the tests specific to org-super-agenda. For that, I’m currently waiting for MELPA to build the version of org-super-agenda that has the fix applied, so I can install that into the test sandbox, and then those two tests should pass also.
Enumerate and test parameters and potentially unsafe types
- State “DONE” from “UNDERWAY” [2020-11-11 Wed 23:26]
- State “UNDERWAY” from [2020-11-11 Wed 23:15]
[[org-ql-search:todo:?]]
(insert (url-hexify-string (concat "buffers-files=" (prin1-to-string '((lambda () (message "AHA")))))))- [X] Buffers-Files: Expanded by
org-ql-view--expand-buffers-files:- [X] Quoted lambda: (safe) org-ql-search:todo:?buffers-files%3D%28lambda%20nil%20%28message%20%22AHA%22%29%29
- [X] Unquoted lambda: (safe) org-ql-search:todo:?buffers-files%3D%28lambda%20nil%20%28message%20%22AHA%22%29%29
- [X] Quoted lambda in list (safe): org-ql-search:todo:?buffers-files%3D%28%28quote%20%28lambda%20nil%20%28message%20%22AHA%22%29%29%29%29
- [X] Unquoted lambda in list: (safe) org-ql-search:todo:?buffers-files%3D%28%28lambda%20nil%20%28message%20%22AHA%22%29%29%29
- [X] Groups
- [X] Quoted lambda (safe): org-ql-search:todo:?super-groups%3D%28lambda%20nil%20%28message%20%22AHA%22%29%29
- [X] Unquoted lambda (safe): org-ql-search:todo:?super-groups%3D%28lambda%20nil%20%28message%20%22AHA%22%29%29
- [X] Quoted expression (safe): org-ql-search:todo:?super-groups%3D%28message%20%22AHA%22%29
- [X] Unquoted expression (safe): org-ql-search:todo:?super-groups%3D%22AHA%22
- [X]
:predselector (UNSAFE, but caught with new org-super-agenda variable): org-ql-search:todo:?super-groups%3D%28%28%3Apred%20%28lambda%20%28_%29%20%28message%20%22AHA%22%29%29%29%29 - [X]
:auto-mapselector (UNSAFE, but caught with new org-super-agenda variable): org-ql-search:todo:?super-groups%3D%28%28%3Aauto-map%20%28lambda%20%28_%29%20%28message%20%22AHA%22%29%29%29%29
- [X] Title
- [X] Quoted lambda (produces the same encoded value as unquoted lambda): (safe) org-ql-search:todo:?title%3D%28lambda%20%28_%20_%29%20%28message%20%22AHA%22%29%29
- [X] Unquoted lambda: (safe) org-ql-search:todo:?title%3D%28lambda%20%28_%20_%29%20%28message%20%22AHA%22%29%29
- [X] Expression: (safe) org-ql-search:todo:?title%3D%28message%20%22AHA%22%29
- [X] Sort
- [X] Bare, quoted lambda: (maybe unsafe, but caught now): org-ql-search:todo:?sort%3D%28lambda%20%28_%20_%29%20%28message%20%22AHA%22%29%29
- [X] Bare, unquoted lambda: (UNSAFE, but caught now): org-ql-search:todo:?sort%3D%28lambda%20%28_%20_%29%20%28message%20%22AHA%22%29%29
- [X] Quoted lambda in list: (maybe unsafe, but caught now): org-ql-search:todo:?sort%3D%28%28quote%20%28lambda%20%28_%20_%29%20%28message%20%22AHA%22%29%29%29%29
- [X] Unquoted lambda in a list: (UNSAFE, but caught now): org-ql-search:todo:?sort=((lambda%20nil%20(message%20”AHA”)))
For the query expression:
- String queries are parsed by the PEG parsing function (which I will probably rename soon), which should only allow known Org QL predicates, not arbitrary functions. For example:
(org-ql--plain-query "message:AHA") ;;=> (regexp "message:AHA") (org-ql--plain-query '(message "AHA")) ;;=> (wrong-type-argument stringp (message "AHA")) (org-ql--plain-query "(message \"AHA\"") ;;=> (and (regexp "(message") (regexp "AHA"))
- Sexp queries already prompt for confirmation, unless the user has set
org-ql-view-ask-unsafe-linksto nil.
[2020-11-11 Wed 23:27] That’s all the parameters and all the types that I can think to test.
[#A] Views: Multiple sorters are not preserved
[#A] Make dynamic blocks warn about sexp queries
[2020-11-12 Thu 05:22] I guess to be super-extra careful, just in case someone had org-update-all-dblocks in the before-save-hook or something.
[2020-11-17 Tue 00:25] It warns and the warning is tested.
[#A] Add Emacs 27.1 to test.yml
[2020-11-16 Mon 05:22] Also releasing makem.sh 0.2 with this change.
[#A] Fix org-ql-view--link-open on Org 9.3+
The version of Org in my personal that passes a URL-decoded string (i.e. as if run through url-unhex-string) as the argument to org-ql-view--link-open. But Org 9.3 in Emacs 27.1 passes a non-URL-decoded string, so org-ql-view--link-open needs to pass it through url-unhex-string itself.
But I don’t know which version of Org that changed in. I’m comparing the function org-open-at-point, but it’s a 114-line function, and in neither version does it call url-unhex-string, so whatever code decodes the string must be elsewhere.
I do recall something about links changing in Org 9.3 (or thereabouts), so that was probably part of it. Maybe I can find it in the release notes. I just need to know basically which version it happened in.
I noticed because the CI tests on GitHub show the link-safety tests failing on the Emacs snapshot version. However, I think they’re not currently vulnerable on that Org version, because the link parameters fail to be parsed correctly, so all the arguments to org-ql-search should end up being nil.
[2020-11-14 Sat 20:41] I should probably do something like this in org-ql-view--link-open:
(when (version<= "9.3" (org-version))
;; Org 9.3+ makes a backward-incompatible change to link escaping.
;; I don't think it would be a good idea to try to guess whether
;; the string received by this function was made with or without
;; that change, so we'll just test the current version of Org.
;; Any links created with older Org versions and then opened with
;; newer ones will have to be recreated.
(setf path (url-unhex-string path)))But, first, I should write tests for saving and opening links, so it can actually be tested on different versions automatically.
[2020-11-16 Mon 05:12] Finally, all of the tests pass on my Org version and on 9.3. And I tested for all the combinations of link and bookmark saving/opening I could think of. I hope they work and are safe.
[#A] Write tests for storing/opening links
[2020-11-16 Mon 05:11] Took way longer than I expected. I hope it was worth it.
[#A] Check Org release notes for link changes
[2020-11-13 Fri 22:44] From the changelog:
Change bracket link escaping syntax
Org used to percent-encode sensitive characters in the URI part of the bracket links.
Now, escaping mechanism uses the usual backslash character, according to the following rules, applied in order:
- All consecutive \ characters at the end of the link must be escaped;
- Any ] character at the very end of the link must be escaped;
- All consecutive \ characters preceding ][ or ]] patterns must be escaped;
- Any ] character followed by either [ or ] must be escaped;
- Others ] and \ characters need not be escaped.
When in doubt, use the function org-link-escape in order to turn a link string into its properly escaped form.
The following function will help switching your links to the new syntax:
(defun org-update-link-syntax (&optional no-query) “Update syntax for links in current buffer. Query before replacing a link, unless optional argument NO-QUERY is non-nil.” (interactive “P”) (org-with-point-at 1 (let ((case-fold-search t)) (while (re-search-forward “\[\^*?%\(?:2[05]\|5[BD]\)” nil t) (let ((object (save-match-data (org-element-context)))) (when (and (eq ‘link (org-element-type object)) (= (match-beginning 0) (org-element-property :begin object))) (goto-char (org-element-property :end object)) (let* ((uri-start (+ 2 (match-beginning 0))) (uri-end (save-excursion (goto-char uri-start) (re-search-forward “\][][]” nil t) (match-beginning 0))) (uri (buffer-substring-no-properties uri-start uri-end))) (when (or no-query (y-or-n-p (format “Possibly obsolete URI syntax: %S. Fix? ” uri))) (setf (buffer-substring uri-start uri-end) (org-link-escape (org-link-decode uri)))))))))))
The old org-link-escape and org-link-unescape functions have been renamed into org-link-encode and org-link-decode.
This is exactly the kind of breaking, backwards-incompatible change that I’ve said should mandate a major-version increment. It’s not only a change in Org’s code, and a change that affects third-party packages, but it’s a change in the file format!
Is it even possible to support both Org 9.3+ and earlier versions at the same time?
This change doesn’t even seem to make sense to me. Percent-encoding solves so many problems in a simple way: pass a string to the encoding function on the way in, and to the decoding function on the way out. Now, instead of a simple, standard way of encoding links, there’s a list of Org-specific rules and Org-specific encoding/decoding functions. What is gained this way?
[#A] Fix query-sexp-to-string function’s handling of, e.g. descendants
[2020-11-14 Sat 20:45] Fixed in 89ff02a1501b53b4e20cdc66a8fcaa37e7d15734.
[#A] Helm command
In branch wip/helm-org-ql. Works really well, should add it and demonstrate it.
Add
Demonstrate
Parsing non-Lisp queries
[2019-09-12 Thu 12:56] Lisp is so much easier to deal with, but some people don’t like parentheses. So I’m trying to add a non-Lisp-style query syntax. It gets complicated. The peg library helps, but its documentation is sparse and incomplete. This seems to work fairly well for single-token queries, but I’m not sure if I can or should cram it all into one parser, or use separate ones for certain keywords.
(-let* ((input "todo:check|someday")
(input "tags:universe+space")
(input "heading:\"spaced phrase\"")
(input "")
(input "heading:\"spaced phrase\"+another")
combinator
(parsed (peg-parse-string ((predicate (substring keyword) ":" (opt args))
(keyword (or "heading" "tags" "todo" "property"))
(args (+ (and (or quoted-arg unquoted-arg) (opt separator))))
(quoted-arg "\"" unquoted-arg "\"")
(unquoted-arg (substring (+ (not (or separator "\"")) (any))))
(separator (or (and "|" (action (setf combinator 'or)))
(and "+" (action (setf combinator 'and)))
(and ":" (action (setf combinator 'arg))))))
input 'noerror))
((predicate . args) (nreverse parsed)))
(when predicate
(list :predicate predicate :args args :combinator combinator)))
;;=> (:predicate "heading" :args ("spaced phrase" "another" t) :combinator and)I don’t know where the t is coming from.
The next step is to make it work with multi-token queries. It needs to handle all of the tokens in one parser so it can handle quoted phrases (if we split on spaces, it would split quoted phrases). But that makes getting the arguments out of it more difficult. Probably need to do something like this:
(-let* ((input "todo:check|someday")
(input "tags:universe+space")
(input "heading:\"spaced phrase\"")
(input "")
(input "heading:\"spaced phrase\"+another")
combinator
(parsed (peg-parse-string ((query (+ (or (and predicate `(pred args -- (list :predicate pred :args args)))
(and plain-string `(s -- (list :predicate 'regexp :args s))))
(opt (syntax-class whitespace))))
(plain-string (substring (+ (not (syntax-class whitespace)) (any))))
(predicate (substring keyword) ":" (opt args))
(keyword (or "heading" "tags" "todo" "property"))
(args (+ (and (or quoted-arg unquoted-arg) (opt separator))))
(quoted-arg "\"" unquoted-arg "\"")
(unquoted-arg (substring (+ (not (or separator "\"")) (any))))
(separator (or (and "|" (action (setf combinator 'or)))
(and "+" (action (setf combinator 'and)))
(and ":" (action (setf combinator 'arg))))))
input 'noerror)))
parsed)In which lists are pushed onto the stack and returned, rather than strings. But I don’t understand yet exactly how to use the var forms to consume input from the “value stack”; I need to study the examples more. I’m also not sure if that will even work with a variable number of arguments.
This seems to work, but we’ll have to parse the args again in a separate step:
(-let* ((input "todo:check|someday")
(input "tags:universe+space")
(input "heading:\"spaced phrase\"")
(input "")
(input "heading:\"spaced phrase\"+another")
(input "heading:\"spaced phrase\"+another todo:check")
combinator
(parsed (peg-parse-string ((query (+ (or (and predicate `(pred args -- (list :predicate pred :args args)))
(and plain-string `(s -- (list :predicate 'regexp :args s))))
(opt (+ (syntax-class whitespace) (any)))))
(plain-string (substring (+ (not (syntax-class whitespace)) (any))))
(predicate (substring keyword) ":" (opt args))
(keyword (or "heading" "tags" "todo" "property"))
(args (substring (+ (and (or quoted-arg unquoted-arg) (opt separator)))))
(quoted-arg "\"" (+ (not (or separator "\"")) (any)) "\"")
(unquoted-arg (+ (not (or separator "\"" (syntax-class whitespace))) (any)))
(separator (or (and "|" (action (setf combinator 'or)))
(and "+" (action (setf combinator 'and)))
(and ":" (action (setf combinator 'arg))))))
input 'noerror)))
parsed)
;;=> (t (:predicate "todo" :args "check") (:predicate "heading" :args "\"spaced phrase\"+another"))Well, a bit of fiddling (lots of trial-and-error required) produced this:
(-let* ((input "todo:check|someday")
(input "tags:universe+space")
(input "heading:\"spaced phrase\"")
(input "")
(input "heading:\"spaced phrase\"+another")
(input "heading:\"spaced phrase\"+another todo:check")
combinator
(parsed (peg-parse-string ((query (+ (or (and predicate `(pred args -- (list :predicate pred :args args)))
(and plain-string `(s -- (list :predicate 'regexp :args s))))
(opt (+ (syntax-class whitespace) (any)))))
(plain-string (substring (+ (not (syntax-class whitespace)) (any))))
(predicate (substring keyword) ":" (opt args))
(keyword (or "heading" "tags" "todo" "property"))
(args (list (+ (and (substring (or quoted-arg unquoted-arg)) (opt separator)))))
(quoted-arg "\"" (+ (not (or separator "\"")) (any)) "\"")
(unquoted-arg (+ (not (or separator "\"" (syntax-class whitespace))) (any)))
(separator (or (and "|" (action (setf combinator 'or)))
(and "+" (action (setf combinator 'and)))
(and ":" (action (setf combinator 'arg))))))
input 'noerror)))
parsed)
;;=> (t (:predicate "todo" :args ("check")) (:predicate "heading" :args ("\"spaced phrase\"" "another")))That seems pretty usable!
[#B] Add a :with-time argument to timestamp predicates
- In branch
wip/with-time.
Which would obviate the need to use the ~org-scheduled-time-hour-regexp~ to find entries scheduled for a certain time of day.
Probably should do this after Optimized, date-specific timestamp regexps.
Also relevant to examples: add two new examples by mskorzhinskiy · Pull Request #73 · alphapapa/org-ql · GitHub.
[2021-06-17 Thu 20:01] Implemented for scheduled in branch wip/with-time2 (I should check for my own prior work before starting, haha). It’s fairly simple, and since I haven’t finished the date-specific timestamp regexps branch yet, I might as well do this first, because it’s useful. The other branch has some needed work too, so I should fold it into the second branch.
[2021-06-17 Thu 20:11] Merged second branch into first and deleted second.
[2021-06-19 Sat 03:30] Everything seems to work. Merged and pushed to master.
Merge to master
- [X] Probably squash merge.
- [X] Push
Update docs
Readme
deadline
ts
scheduled
planning
General date/time predicate info
Docstrings
deadline
ts
scheduled
planning
Ensure changes to these predicates are consistent
deadline
Tests
scheduled
- [X] Like
planning, select the regexp inorg-ql--query-predicate. - [X] Use new regexps.
Tests
planning
Tests
regexp defvar
ts
[2021-06-18 Fri 00:00] Had to add a bunch of extra regexps, but probably needed to do that anyway. Seems to work…
Tests
[2021-06-17 Thu 23:59] The tests pass and seem to work correctly.
[#B] “Node” caching
[2019-09-05 Thu 12:30] At each node checked by a predicate, make a struct that stores attributes we can query for, as well as parent node position. This would let us speed up ancestor-based queries, like (ancestor (todo "WAITING")). Ideally it would also serve as the tag hierarchy cache.
It would probably be an all-encompassing system, because predicates would need to refer to the cached node when available. So maybe the struct should be like ts-defstruct, with lazy, caching accessors, which would move some of the predicates’ code into the accessors.
Maybe a good improvement to make later, after the project is more developed.
[2019-10-07 Mon 13:08] This has basically been implemented in be2bf6df316b96b3ed56851b8ffe0e227796b621, but as functions and values rather than with structs. It remains to be seen how this works with ancestor queries, but I suspect it will help a lot.
Struct PoC code
This works okay (except the priority accessor needs to be fixed, because Org priorities are awkward to get). I’m guessing all the extra function calls would make it undesirable in cases of returning many results, but it’s a flexible concept that makes sorting easy.
(ts-defstruct org-ql-node
file position marker
(level
nil :accessor-init (org-with-point-at (org-ql-node-marker struct)
(org-outline-level)))
(heading
nil :accessor-init (org-with-point-at (org-ql-node-marker struct)
;; TODO: Org 9.2+ adds 2 more args to `org-get-heading'.
(org-get-heading t t)))
(priority
nil :accessor-init (org-with-point-at (org-ql-node-marker struct)
(org-get-priority )))
(tags
nil :accessor-init (org-with-point-at (org-ql-node-marker struct)
(->> (org-ql--tags-at (point))
-flatten
(delq 'org-ql-nil))))
(todo
nil :accessor-init (org-with-point-at (org-ql-node-marker struct)
(org-get-todo-state)))
(outline-path
nil :accessor-init (org-with-point-at (org-ql-node-marker struct)
(org-split-string (org-format-outline-path (org-get-outline-path)
nil nil "�")
"�"))))
(defcustom helm-org-ql-sort
'(org-ql-node-priority org-ql-node-todo)
"FIXME"
)
(cl-defun helm-org-ql (buffers-files &optional no-and)
"Display results in BUFFERS-FILES for an `org-ql' query using Helm.
Interactively, search the current buffer.
NOTE: Atoms in the query are turned into strings where
appropriate, which makes it unnecessary to type quotation marks
around words that are intended to be searched for as indepenent
strings.
Also, unless NO-AND is non-nil (interactively, with prefix), all
query tokens are wrapped in an implied (and) form. This is
because a query must be a sexp, so when typing multiple clauses,
either (and) or (or) would be required around them, and (and) is
typically more useful, because it narrows down results.
For example, this raw input:
Emacs git
Is transformed into this query:
(and \"Emacs\" \"git\")
However, quoted strings remain quoted, so this input:
\"something else\" (tags \"funny\")
Is transformed into this query:
(and \"something else\" (tags \"funny\"))"
(interactive (list (current-buffer) current-prefix-arg))
(let ((helm-input-idle-delay helm-org-ql-input-idle-delay))
(helm :sources
(helm-build-sync-source "helm-org-ql-agenda-files"
:candidates (lambda ()
(let* ((query (helm-org-ql--input-to-query helm-pattern no-and))
(window-width (window-width (helm-window))))
(when query
(let ((results (org-ql-select buffers-files
query
:action '(make-org-ql-node :marker (point-marker)))))
(when helm-org-ql-sort
(dolist (sorter (reverse helm-org-ql-sort))
(setf results (sort results sorter))))
(cl-loop for it in-ref results
do (setf it (concat (buffer-name (org-ql-node-file it)) ":"
(or (org-ql-node-todo it) "")
(or (org-ql-node-priority it) "")
(org-ql-node-heading it) "\\"
(org-ql-node-outline-path it))))
results))))
:match #'identity
:fuzzy-match nil
:multimatch nil
:volatile t
:action #'helm-org-goto-marker))))[#B] Define predicates with a macro
[2020-11-21 Sat 16:40] i.e. the macro defines the predicate, preamble, and normalizer in one form. WIP on the wip/define-predicate branch. Seems to be working well so far.
[2020-11-22 Sun 17:17] Everything is converted, everything works, and all the tests pass. Worked out just as I hoped. Will merge for 0.6.
[#A] Merge new defpred into master
Test defining custom predicates in user config
Show how to define custom predicates
Tutorial is published.
Run tests
Convert all predicates to new macro
[#B] Move this notes file into an orphan meta/notes branch
[2020-11-12 Thu 03:17] Will probably have to merge or delete some WIP branches first, otherwise they’ll probably get conflicts.
[2020-11-22 Sun 19:40] Did this a day or two ago. Didn’t rebase all the WIP branches, but they shouldn’t be any trouble.
[#B] Quickly change sorting/grouping in search views
With caching, the search doesn’t need to be repeated, so resorting/regrouping can be very fast.
Byte-compile lambdas
- State “DONE” from [2018-05-09 Wed 17:30]
elfeed-search--update-list byte-compiles lambdas returned by elfeed-search-compile-filter. Maybe I could do something like this too.
If I can get this working, I should profile it to see what difference it makes.
Profiling
Going to try byte-compiling the predicate function:
(elp-profile 10 nil (org-agenda-ng "~/src/emacs/org-super-agenda/test/test.org"
(and (or (date :date '= (org-today))
(date :deadline '<= (+ org-deadline-warning-days (org-today)))
(date :scheduled '<= (org-today)))
(not (apply #'todo org-done-keywords-for-agenda)))))| Function | Times called | Total time | Average time |
|---|---|---|---|
| org-agenda-ng–agenda | 10 | 0.8370581039 | 0.0837058104 |
| org-agenda-finalize-entries | 10 | 0.652886608 | 0.0652886608 |
| org-super-agenda–filter-finalize-entries | 10 | 0.641794501 | 0.0641794501 |
| org-super-agenda–group-items | 10 | 0.636057006 | 0.0636057006 |
| org-super-agenda–group-dispatch | 130 | 0.631911849 | 0.0048608603 |
| org-super-agenda–group-tag | 50 | 0.592883869 | 0.0118576773 |
| list | 2720 | 0.5792795169 | 0.0002129704 |
| mapcar | 331 | 0.2333591920 | 0.0007050126 |
| org-agenda-ng–filter-buffer | 10 | 0.09247626 | 0.009247626 |
| org-agenda-ng–format-element | 150 | 0.0649320479 | 0.0004328803 |
| org-entry-get | 860 | 0.0408285349 | 4.747…e-05 |
| org-agenda-ng–date-p | 910 | 0.0385646249 | 4.237…e-05 |
| org-element-headline-parser | 150 | 0.0374417470 | 0.0002496116 |
| org-is-habit-p | 270 | 0.0290107389 | 0.0001074471 |
| org–property-local-values | 270 | 0.0268615979 | 9.948…e-05 |
| org-get-property-block | 270 | 0.0244613309 | 9.059…e-05 |
| org-get-tags-at | 150 | 0.017875864 | 0.0001191724 |
| org-super-agenda–group-habit | 10 | 0.015910656 | 0.0015910655 |
| mapc | 2540 | 0.0158616290 | 6.244…e-06 |
| org-agenda-ng–add-faces | 150 | 0.0143329670 | 9.555…e-05 |
Now the same thing without byte-compiling:
(elp-profile 10 nil (org-agenda-ng "~/src/emacs/org-super-agenda/test/test.org"
(and (or (date :date '= (org-today))
(date :deadline '<= (+ org-deadline-warning-days (org-today)))
(date :scheduled '<= (org-today)))
(not (apply #'todo org-done-keywords-for-agenda)))))| Function | Times called | Total time | Average time |
|---|---|---|---|
| org-agenda-ng–agenda | 10 | 0.846645537 | 0.0846645537 |
| org-agenda-finalize-entries | 10 | 0.662896805 | 0.0662896805 |
| sort | 40 | 0.591123256 | 0.0147780814 |
| org-entries-lessp | 400 | 0.5901201620 | 0.0014753004 |
| mapcar | 201 | 0.2318270599 | 0.0011533684 |
| org-agenda-ng–filter-buffer | 10 | 0.092519787 | 0.0092519787 |
| org-super-agenda–filter-finalize-entries | 10 | 0.0664278040 | 0.0066427804 |
| org-agenda-ng–format-element | 150 | 0.064658994 | 0.0004310599 |
| org-super-agenda–group-items | 10 | 0.0602504089 | 0.0060250408 |
| org-super-agenda–group-dispatch | 130 | 0.0561904470 | 0.0004322342 |
| org-entry-get | 860 | 0.0437458889 | 5.086…e-05 |
| org-agenda-ng–date-p | 910 | 0.0382623409 | 4.204…e-05 |
| org-element-headline-parser | 150 | 0.0374662920 | 0.0002497752 |
| org-is-habit-p | 270 | 0.0320861079 | 0.0001188374 |
| org–property-local-values | 270 | 0.0298690430 | 0.0001106260 |
| org-get-property-block | 270 | 0.0274716649 | 0.0001017469 |
| org-super-agenda–group-habit | 10 | 0.019117901 | 0.0019117901 |
| org-get-tags-at | 150 | 0.0178958930 | 0.0001193059 |
| mapc | 2470 | 0.0150361130 | 6.087…e-06 |
| org-agenda-ng–add-faces | 150 | 0.0143092169 | 9.539…e-05 |
Virtually indistinguishable. Going to try moving the byte-compile call from the org-agenda-ng macro to other places…
(elp-profile 10 nil (org-agenda-ng "~/src/emacs/org-super-agenda/test/test.org"
(and (or (date :date '= (org-today))
(date :deadline '<= (+ org-deadline-warning-days (org-today)))
(date :scheduled '<= (org-today)))
(not (apply #'todo org-done-keywords-for-agenda)))))| Function | Times called | Total time | Average time |
|---|---|---|---|
| org-agenda-ng–agenda | 10 | 0.8476316779 | 0.0847631678 |
| mapcar | 331 | 0.8159452220 | 0.0024650913 |
| org-agenda-ng–filter-buffer | 10 | 0.674217912 | 0.0674217912 |
| org-element-headline-parser | 150 | 0.6171195889 | 0.0041141305 |
| line-beginning-position | 620 | 0.5802579680 | 0.0009358999 |
| org-agenda-finalize-entries | 10 | 0.082065157 | 0.0082065157 |
| org-super-agenda–filter-finalize-entries | 10 | 0.0708772279 | 0.0070877227 |
| org-super-agenda–group-items | 10 | 0.065523103 | 0.0065523103 |
| org-agenda-ng–format-element | 150 | 0.0652783740 | 0.0004351891 |
| org-super-agenda–group-dispatch | 130 | 0.0614253589 | 0.0004725027 |
| org-entry-get | 860 | 0.0494023029 | 5.744…e-05 |
| org-agenda-ng–date-p | 910 | 0.0388435519 | 4.268…e-05 |
| org-is-habit-p | 270 | 0.0375687549 | 0.0001391435 |
| org–property-local-values | 270 | 0.0353892929 | 0.0001310714 |
| org-get-property-block | 270 | 0.0329700440 | 0.0001221112 |
| org-super-agenda–group-habit | 10 | 0.024468601 | 0.0024468601 |
| re-search-backward | 1500 | 0.0186344089 | 1.242…e-05 |
| org-get-tags-at | 150 | 0.0180038809 | 0.0001200258 |
| mapc | 2540 | 0.0156518099 | 6.162…e-06 |
| org-agenda-ng–add-faces | 150 | 0.0144141080 | 9.609…e-05 |
Doesn’t seem to make any difference.
Document/figure out tag inheritance
I think it should probably be enabled in most cases, to avoid missing results that users would expect to find, but it will reduce performance in some cases, so users should be able to turn it off when they don’t need it.
[2018-06-12 Tue 14:32] The docstring for org-map-entries says:
If your function needs to retrieve the tags including inherited tags at the current entry, you can use the value of the variable ‘org-scanner-tags’ which will be much faster than getting the value with ‘org-get-tags-at’. If your function gets properties with ‘org-entry-properties’ at the current entry, bind ‘org-trust-scanner-tags’ to t around the call to ‘org-entry-properties’ to get the same speedup. Note that if your function moves around to retrieve tags and properties at a different entry, you cannot use these techniques.
[2019-09-26 Thu 21:31] Handled with the tag caching recently implemented.
[#B] Dual matching with regexp and predicates
Note: This is underway in the =preamble-re= branch.
Searching and matching could be sped up by constructing a regexp that searches directly to the next possible match, and then matching with predicate functions.
For example, a search like:
(org-ql (org-agenda-files)
(and (regexp "lisp")
(scheduled < today)))Only entries that contain the word lisp can be matches, and searching each entry for that word is wasteful. Instead, we could search the buffer for the next occurrence of lisp, then check the scheduled date for that entry.
This would require processing the predicate to pull out matchers that can be done as buffer-wide regexps, e.g. regexp, heading-regexp, todo, and possibly tags. Org has some regexp-building functions that might make this fairly easy, and then we could probably use rx to make an optimized version of the regexp. It would also require some refactoring to the searching that would go directly to regexp matches when possible, rather than checking every entry with the predicate.
[2019-07-16 Tue 11:14] Made new branch preamble-re-new based on current master. Seems to work well. Here’s some code for testing and comparing performance (bench-multi-lets is from here).
[2019-07-16 Tue 11:56] Going to merge to master as 0.2, so marking this as done, even though there’s a bit more that can be done from here.
Benchmark code
(cl-defmacro org-ql-preamble-bench (&key query (file "tests/data.org") (times 10))
`(bench-multi-lets :times ,times :ensure-equal t
:lets (("preamble" ((org-ql-use-preamble t)))
("no preamble" ((org-ql-use-preamble nil))))
:forms ((,(prin1-to-string query) (org-ql-select,file
',query
:action (lambda () (org-get-heading t t)))))))(org-ql-preamble-bench :query (regexp "Emacs") :times 100)| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| preamble: (regexp “Emacs”) | 1.22 | 0.141767 | 0 | 0 |
| no preamble: (regexp “Emacs”) | slowest | 0.172398 | 0 | 0 |
(org-ql-preamble-bench :file "~/org/inbox.org" :query (regexp "Emacs") :times 5)| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| preamble: (regexp “Emacs”) | 1.59 | 2.011043 | 0 | 0 |
| no preamble: (regexp “Emacs”) | slowest | 3.206370 | 0 | 0 |
(org-ql-preamble-bench :file "~/org/inbox.org" :query (and (regexp "Emacs") (todo)) :times 5)| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| preamble: (and (regexp “Emacs”) (todo)) | 1.59 | 2.211503 | 0 | 0 |
| no preamble: (and (regexp “Emacs”) (todo)) | slowest | 3.512741 | 0 | 0 |
(org-ql-preamble-bench :file "~/org/inbox.org" :query (and (regexp "Emacs") (todo) (scheduled)) :times 5)| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| preamble: (and (regexp “Emacs”) (todo) (scheduled)) | 1.69 | 2.042456 | 0 | 0 |
| no preamble: (and (regexp “Emacs”) (todo) (scheduled)) | slowest | 3.453756 | 0 | 0 |
(org-ql-preamble-bench :file "~/org/inbox.org" :query (todo "WAITING") :times 2)| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| preamble: (todo “WAITING”) | 15.60 | 0.070684 | 0 | 0 |
| no preamble: (todo “WAITING”) | slowest | 1.102722 | 0 | 0 |
Wow, that’s a huge improvement!
Operate on list of heading positions
- State “DONE” from [2018-05-10 Thu 15:02]
[2017-12-31 Sun 17:54] I wonder if, instead of parsing the whole buffer with org-element-parse-buffer, we could simply work on a list of heading positions, e.g. a loop would search forward to the next heading position, then call whatever predicates it needed at the heading’s position, using save-excursion around each function call. The predicates would need to be updated to get their data from the buffer, instead of using org-element-property, but that wouldn’t be hard.
[2018-05-10 Thu 15:01] I already changed to using buffer-parsing predicates instead of org-element-parse-buffer.
Use macros for date
- State “DONE” from [2018-05-10 Thu 14:59]
If I made the date selector a macro, I could avoid the need to quote the comparator.
Also, maybe instead of having a single date selector, I should have scheduled, deadline, etc.
Checklists
Commits
To complete before and after pushing any commit.
Additions
When committing an additional feature:
- [ ] Make WIP branch
- [ ] Check repo for closing issues
- [ ] Check magit-todos list of items branched from master
- [ ] Lint
- [ ] Test
- [ ] Locally
- [ ] On different Emacs versions with GitHub CI (very important)
- [ ] Update docs
- [ ] Update changelog
- [ ] Mention closing issues (optionally)
- [ ] Commit
- [ ] Mention closing issues in commit message
- [ ] Merge to master
- [ ] Push master
- [ ] Close related tasks in this file
- [ ] Delete WIP branch
Fixes
When committing a fix:
- [ ] Check repo for closing issues
- [ ] Check magit-todos list of items branched from master
- [ ] Lint
- [ ] Test
- [ ] Locally
- [ ] On different Emacs versions with GitHub CI (very important)
- [ ] Update changelog
- [ ] Mention changes
- [ ] Mention closing issues
- [ ] Commit
- [ ] Mention closing issues in commit message
- [ ] Push
- [ ] Close related tasks in this file
Release template
- [ ] Make WIP branch
- [ ]
Metapre-release commit- [ ] Update version numbers
- [ ]
org-ql.el - [ ]
helm-org-ql.el - [ ]
README.org
- [ ]
- [ ] Update version numbers
- [ ] Complete commit checklist
- [ ] Changelog entry
- [ ] Merge to stable branch
- [ ] Non-fast-forward merge WIP branch into stable branch
- [ ] Tag and sign merge commit
- [ ] Push stable branch
- [ ] Push tags
- [ ] Merge to master or make stable branch
- [ ] Push master/stable
- [ ] Delete WIP branch
- [ ] Make GitHub release
- [ ] Announce
- [ ] org-mode mailing list
Archive
Release 0.4.9
- [X] Complete commit checklist
- [X] Changelog entry
- [X] Update version numbers
- [X]
org-ql.el - [X]
helm-org-ql.el(N/A) - [X]
README.org
- [X]
- [X] Tag and sign
- [X] Push
- [X] Merge to master
Release 0.5
- [X] Complete commit checklist
- [ ] Changelog entry
- [X] Update version numbers
- [X]
org-ql.el - [X]
helm-org-ql.el(N/A yet) - [X]
README.org
- [X]
- [X] Tag and sign
- [X] Push
- [X] Merge to master or make stable branch
- [X] Push master/stable
Release 0.5.1
- [X] Complete commit checklist
- [X] Changelog entry
- [X] Update version numbers
- [X]
org-ql.el - [X]
helm-org-ql.el - [X]
README.org
- [X]
- [X] Tag and sign
- [X] Push
- [X] Merge to master or make stable branch
- [X] Push master/stable
Release 0.5.2
- [X] Make WIP branch
- [X]
Metapre-release commit- [X] Update version numbers
- [X]
org-ql.el - [X]
helm-org-ql.el - [X]
README.org
- [X]
- [X] Update version numbers
- [X] Complete commit checklist
- [X] Changelog entry
- [X] Merge to stable branch
- [X] Non-fast-forward merge WIP branch into stable branch
- [X] Tag and sign merge commit
- [X] Push stable branch
- [X] Merge to master or make stable branch
- [X] Push master/stable
- [X] Delete WIP branch
Release: 0.6
- [X] Make WIP branch
- [X]
Metapre-release commit- [X] Update version numbers
- [X]
org-ql.el - [X]
helm-org-ql.el - [X]
README.org
- [X]
- [X] Update version numbers
- [X] Complete commit checklist
- [X] Changelog entry
- [ ] Merge to stable branch
- [ ] Non-fast-forward merge WIP branch into stable branch
- [ ] Tag and sign merge commit
- [X] Push stable branch
- [X] Push tags
- [X] Merge to master or make stable branch
- [X] Push master/stable
- [X] Delete WIP branch
- [X] Make GitHub release
- [X] Announce
- [X] Reddit
- [X] org-mode mailing list
Examples / testing
(org-agenda-ng org-agenda-files
(and (or (date :deadline '<= (org-today))
(date :scheduled '<= (org-today)))
(not (apply #'todo org-done-keywords-for-agenda)))
((group (tags "bills"))
(group (todo "SOMEDAY"))))
(org-agenda-ng org-agenda-files
(and (or (date :deadline '<= (org-today))
(date :scheduled '<= (org-today)))
(not (apply #'todo org-done-keywords-for-agenda))))
(org-agenda-ng "~/org/main.org"
(and (or (date :deadline '<= (org-today))
(date :scheduled '<= (org-today)))
(not (apply #'todo org-done-keywords-for-agenda))))
(org-ql org-agenda-files
(and (todo "SOMEDAY")
(tags "Emacs")))
(org-ql org-agenda-files
(and (todo "SOMEDAY")
(tags "Emacs")
(priority >= "B")))
(org-ql "~/org/main.org"
(and (or (tags "Emacs")
(priority >= "B"))
(not (done))))
(org-ql "~/org/main.org"
(and (or (tags "Emacs")
(priority >= "B"))
(done)))Property matching
(org-agenda-ng "~/src/emacs/org-super-agenda/test/test.org"
(property "agenda-group"))
(org-agenda-ng "~/src/emacs/org-super-agenda/test/test.org"
(property "agenda-group" "plans"))Regexp matching
(org-ql "~/src/emacs/org-super-agenda/test/test.org"
(regexp "over"))
(org-agenda-ng "~/src/emacs/org-super-agenda/test/test.org"
(regexp "over"))Screenshot code
(org-super-agenda--test-with-org-today-date "2017-07-08 00:00"
(org-ql "~/src/emacs/org-super-agenda/test/test.org"
(and (or (date = today)
(deadline <=)
(scheduled <= today))
(not (done)))))Sorting
(org-ql "~/src/emacs/org-super-agenda/test/test.org"
(regexp "over")
:sort (priority deadline scheduled))
(org-ql "~/src/emacs/org-super-agenda/test/test.org"
(regexp "over")
:sort (date))
(org-ql "~/src/emacs/org-super-agenda/test/test.org"
(todo)
:sort (todo))In the wild
Alois Janicek
further tweaking org-ql views · AloisJanicek/.doom.d-2nd@41ed108 · GitHub
;; Hydras
(defhydra gtd-agenda (:color blue
:body-pre
(if (aj/has-heading-p +INBOX)
(org-ql-search `(,+INBOX) "*"
:sort '(date))
(org-ql-search (org-agenda-files)
'(todo "NEXT")
:sort '(date priority todo)
:groups '((:auto-category t))))
)
"agenda"
("a" (org-ql-agenda (org-agenda-files)
(and (or (ts-active :on today)
(deadline auto)
(scheduled :to today))
(not (done)))) "agenda")
("t" (org-ql-search (org-agenda-files)
'(todo "TODO")
:sort '(date priority todo)
:groups '((:auto-category t))) "todo")
("n" (org-ql-search (org-agenda-files)
'(todo "NEXT")
:sort '(date priority todo)
:groups '((:auto-category t))) "next")
("h" (org-ql-search (org-agenda-files)
'(todo "HOLD")
:sort '(date priority todo)
:groups '((:auto-category t))) "hold")
("s" (org-ql-search (org-agenda-files)
'(tags "someday")
:sort '(date priority todo)
:groups '((:auto-category t))) "someday")
("r" (org-ql-search (org-agenda-files)
'(ts :from -7 :to today)
:sort '(date priority todo)
:groups '((:auto-ts t))) "recent")
("i" (org-ql-search `(,+INBOX) "*"
:sort '(date)) "inbox")
)Andrea Giugliano
Org Agenda and Your Future, or how to keep score of your long term goals with Org Mode - Where parallels cross
Benson Chu
Experimental agenda view with org-ql · pestctrl/emacs-config@fa30680 · GitHub
("f" "fastdev?"
((org-ql-block '(tags "refile")
((org-agenda-overriding-header "Refile tasks")))
(tags-todo ,(concat dev-tag "/!" (mapconcat #'identity my/active-projects-and-tasks "|"))
((org-agenda-overriding-header "Stuck Projects")
(org-agenda-skip-function 'my/dev-show-stuck-projects)
(org-tags-match-list-sublevels 'indented)
(org-agenda-sorting-strategy
'((agenda category-keep)))))
(tags-todo ,(concat dev-tag "-short" "/!" (mapconcat #'identity my/active-projects-and-tasks "|"))
((org-agenda-overriding-header "Active Projects")
(org-agenda-skip-function 'my/dev-show-active-projects)
(org-tags-match-list-sublevels 'indented)
(org-agenda-sorting-strategy
'((agenda category-keep)))))
(org-ql-block '(and (tags "dev")
(todo "WAIT"))
((org-agenda-overriding-header "Waiting tasks")))
(org-ql-block '(and (tags "dev")
(todo "NEXT"))
((org-agenda-overriding-header "Things to do")))
(agenda ""
((org-agenda-skip-function 'my/agenda-custom-skip)
(org-agenda-span 'day)
(org-agenda-tag-filter-preset (quote (,dev-tag)))
(org-agenda-skip-deadline-if-done t)
(org-agenda-skip-scheduled-if-done t)
(org-super-agenda-groups '((:name "Overdue" :and (:deadline past :log nil))
(:name "Upcoming" :deadline future)
(:name "Should do" :and (:scheduled past :log nil))
(:name "Today" :time-grid t
:and (:not (:and (:not (:scheduled today)
:not (:deadline today)))))))))))Kevin Brubeck Unhammer
org-upcoming-modeline: put upcoming org event in modeline
Mikhail Skorzhinskiy
[2020-12-18 Fri 03:41] Shows a number of complex custom queries and grouping, including his “dashboard” view. Shared at How are folks using org-agenda and org-roam TOGETHER? : emacs.
(setq
org-ql-views
'(("stuck" lambda nil
(interactive)
(org-ql-search
(org-agenda-files)
'(and (tags "story")
(not (tags "ignore"))
(not (done)) ;; Finished stories should be excluded
(not (descendants (todo "NEXT"))) ;; If there are already
;; something in progress
;; it will shown
(and (not (descendants (done))) ;; There are not scheduled not finished items
(not (descendants (scheduled)))))
:narrow nil :super-groups
'((:name "Waiting" :order 8 :todo "WAIT")
(:name "Important" :order 1 :deadline t :priority>= "B")
(:name "Work" :order 2 :tag "work")
(:name "Study" :order 2 :tag "study")
(:name "Stucked" :order 3 :tag "story"))
:title "stuck-projects"))
("reports" lambda nil
(interactive)
(org-ql-search
(org-agenda-files)
'(and (or (tags-local "weekly")
(tags-local "monthly"))
(not (tags "ignore")))
:narrow nil :super-groups
'((:name "Weekly reports" :tag "weekly")
(:name "Monthly reports" :tag "monthly"))
:title "Introspection reports"))
("next" lambda nil
(interactive)
(org-ql-search
(org-agenda-files)
'(and (or (tags-local "refile")
(todo "PROG")
(todo "WAIT")
(todo "NEXT"))
(not (tags "ignore"))
(not (property "linked"))
(not (done)))
:sort '(date)
:narrow nil
:super-groups
`((:name "In progress" :order 1 :todo "PROG")
(:name "Daily" :order 2 :regexp ,org-repeat-re)
(:name "Waiting" :order 3 :todo "WAIT")
(:name "Refile" :order 3 :tag "refile")
(:name "Important" :order 3 :priority>= "B")
(:auto-tags t :order 5))
:title "Next actions"))
("calendar" lambda nil
(interactive)
(org-ql-search
(org-agenda-files)
`(and (ts-active)
(regexp ,org-scheduled-time-hour-regexp)
(not (done)))
:sort '(date)
:narrow nil
:super-groups
'((:auto-planning t))
:title "Calendar"))
("dashboard" lambda nil
(interactive)
(org-ql-search
(org-agenda-files)
'(and (or (ts-active :to today)
(deadline auto)
(todo "PROG")
(and (tags "journal")
(not (tags "weekly"))
(not (tags "monthly"))
(not (tags "yearly"))
(todo)))
(not (todo "WAIT"))
(not (tags "ignore"))
(not (property "linked"))
(not (done)))
:sort '(date)
:narrow nil
:super-groups
`((:name "In progress" :order 1
:tag "monthly" :tag "weekly" :todo "PROG")
(:name "Agenda" :order 2
:deadline t :regexp ,org-scheduled-time-hour-regexp)
(:name "Daily" :order 2
:and (:todo nil :regexp ,org-repeat-re))
(:name "Today" :order 3 :tag "journal")
(:name "Important" :order 3 :priority>= "B")
(:auto-tags t :order 5))
:title "Dashboard"))))Trey Peacock
[2021-07-08 Thu 23:29]
A recent post on reddit asked the question, Why does the recent zettelkasten craze use one file per note rather than one headline per note? Naturally, it brought many differing perspectives and approaches to org-mode usage. I wanted to show my own configuration that largely leverages alphapapa’s wonderful package org-ql. Along with some built-in functionality of org-mode I’m able to search headlines across all of my org files and visit them in indirect buffers automatically. Additionally, you can search backlinks with similar functionality.
This is not meant to be a standalone package by any means, but simply an example of what can be achieved using org-ql. My personal emacs configuration is named baal, so you can safely ignore or rename any references to it.
Add to examples in docs
Should probably ask him first.
Profiling
- {2019-08-29 Thu 06:24} Benchmarking org-ql compared to re-search-forward for getting headings in buffer
- Caching of inherited tags
- Intersecting query results
- More profiling
- Preambles
- Profiling flet across all agenda files
- Profiling flet on a single file
- Profiling org-trust-scanner-tags
- Profiling position-based
- Profiling tags matching
- Using org-element-parse-buffer
- with/without ts.el
[2019-08-29 Thu 06:24] Benchmarking org-ql compared to re-search-forward for getting headings in buffer
Minimal difference, and that’s a very large file, too. On smaller files it’s thousandths of a second.
(with-current-buffer (find-buffer-visiting "~/org/inbox.org")
(bench-multi-lexical :times 1 :ensure-equal t
:forms (("org-ql" (org-ql-select (current-buffer)
'(level 1)
:action '(progn
(font-lock-ensure (line-beginning-position) (line-end-position))
(cons (org-get-heading t) (point)))))
("re-search-forward" (org-with-wide-buffer
(goto-char (point-min))
(when (org-before-first-heading-p)
(outline-next-heading))
(cl-loop while (re-search-forward (rx bol "*" (1+ blank)) nil t)
do (font-lock-ensure (line-beginning-position) (line-end-position))
collect (cons (org-get-heading t) (match-beginning 0))))))))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| re-search-forward | 1.17 | 0.520375 | 0 | 0 |
| org-ql | slowest | 0.608281 | 0 | 0 |
Caching of inherited tags
[2019-09-05 Thu 07:59] Implemented a per-buffer tags cache that seems to significantly speed up tags queries that use tag inheritance. It persists as long as the buffer remains unmodified, and it’s usable from any code as a single function that automatically uses caching. It also returns inherited tags and local tags separately, which could be useful for having separate selectors, one for inherited tags, one for local tags, and one for both.
(defvar org-ql-tags-cache (ht)
"Per-buffer tags cache.
Keyed by buffer. Each value is a cons of the buffer's modified
tick, and another hash table keyed on buffer position, whose
values are a list of two lists, inherited tags and local tags, as
strings.")
(defun org-ql--tags-at (position)
"Return tags for POSITION in current buffer.
Returns cons (INHERITED-TAGS . LOCAL-TAGS)."
;; I'd like to use `-if-let*', but it doesn't leave non-nil variables
;; bound in the else clause, so destructured variables that are non-nil,
;; like found caches, are not available in the else clause.
(if-let* ((buffer-cache (gethash (current-buffer) org-ql-tags-cache))
(modified-tick (car buffer-cache))
(tags-cache (cdr buffer-cache))
(buffer-unmodified-p (eq (buffer-modified-tick) modified-tick))
(cached-result (gethash position tags-cache)))
;; Found in cache: return them.
(pcase cached-result
('org-ql-nil nil)
(_ cached-result))
;; Not found in cache: get tags and cache them.
(let* ((local-tags (or (org-get-tags position 'local)
'org-ql-nil))
(inherited-tags (or (save-excursion
(when (org-up-heading-safe)
(-let* (((inherited local) (org-ql--tags-at (point)))
(inherited-tags (when (or inherited local)
(cond ((and (listp inherited)
(listp local))
(append inherited local))
((cond ((listp inherited) inherited)
((listp local) local)))))))
(when inherited-tags
(->> inherited-tags -non-nil -uniq)))))
'org-ql-nil))
(all-tags (list inherited-tags local-tags)))
;; Check caches again, because they may have been set now.
;; TODO: Is there a clever way we could avoid doing this, or is it inherently necessary?
(setf buffer-cache (gethash (current-buffer) org-ql-tags-cache)
modified-tick (car buffer-cache)
tags-cache (cdr buffer-cache)
buffer-unmodified-p (eq (buffer-modified-tick) modified-tick))
(cond ((or (not buffer-cache)
(not buffer-unmodified-p))
;; Buffer-local tags cache empty or invalid: make new one.
(puthash (current-buffer)
(cons (buffer-modified-tick)
(let ((table (make-hash-table)))
(puthash position all-tags table)
table))
org-ql-tags-cache)
;; Return tags, not the cons put on the buffer-cache.
all-tags)
;; Buffer-local tags cache found, but no result: store this one.
(t (puthash position all-tags tags-cache))))))
(org-ql--defpred tags-cached (&rest tags)
"Return non-nil if current heading has one or more of TAGS (a list of strings)."
;; TODO: Try to use `org-make-tags-matcher' to improve performance. It would be nice to not have
;; to run `org-get-tags' for every heading, especially with inheritance.
(cl-macrolet ((tags-p (tags)
`(and ,tags
(not (eq 'org-ql-nil ,tags)))))
(-let* (((inherited local) (org-ql--tags-at (point))))
(cl-typecase tags
(null (or (tags-p inherited)
(tags-p local)))
(otherwise (or (when (tags-p inherited)
(seq-intersection tags inherited))
(when (tags-p local)
(seq-intersection tags local))))))))Benchmark results:
(let* ((buffers '("~/org/main.org"))
(tags '("Emacs")))
(bench-multi-lexical :times 1 :ensure-equal t
:forms (("uncached" (let ((org-ql-cache (ht)))
(org-ql-select buffers
`(tags ,@tags))))
("cached" (let ((org-ql-cache (ht))
(org-ql-tags-cache (ht)))
(org-ql-select buffers
`(tags-cached ,@tags)))))))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| cached | 6.51 | 0.519871 | 0 | 0 |
| uncached | slowest | 3.386679 | 0 | 0 |
Intersecting query results
An idea that might be helpful for performance in some cases, depending on the query, the data, and whether the query has a preamble. But it looks like it would very rarely be helpful.
(cl-defun org-ql-agenda-intersection (buffers-files queries &key entries sort buffer narrow super-groups)
"Like `org-ql-agenda', but intersects multiple queries."
(declare (indent defun))
(let* ((org-ql-cache (ht))
(entries (->> queries
(--map (org-ql-select buffers-files
it
:action 'element-with-markers
:narrow narrow
:sort sort))
(-reduce #'-intersection))))
(org-ql-agenda--agenda buffers-files queries
:entries entries :super-groups super-groups)))
(bench-multi-lexical :times 1
:forms (("intersection" (let ((org-use-tag-inheritance nil))
(org-ql-agenda-intersection (org-agenda-files)
'((todo "TODO")
(tags "Emacs"))
:sort '(priority deadline)
:super-groups org-super-agenda-groups)))
("normal" (let ((org-use-tag-inheritance nil))
(org-ql-agenda (org-agenda-files)
(and (todo "TODO")
(tags "Emacs"))
:sort (priority deadline)
:super-groups org-super-agenda-groups)))))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| normal | 3.70 | 0.233147 | 0 | 0 |
| intersection | slowest | 0.862512 | 0 | 0 |
Alternative approach
[2019-09-01 Sun 08:17] This is very experimental, but the results are surprising. When the action function returns a fairly simple list, the intersection is very slightly faster. When returning full elements, the intersection is much slower, so that it more than doubles the runtime. I wonder if the element list comparison is short-circuiting, or if it looks at the whole lists, because it seems like it shouldn’t take more than 4-5 list elements before it realizes that two lists don’t match.
Anyway, looks like this approach isn’t viable, at least not without a much more complicated implementation, which probably wouldn’t be worth it.
(let* ((action-fn (lambda ()
(list (current-buffer)
(point)
(substring-no-properties (org-get-heading t t)))))
(files '("~/org/main.org")))
;; NOTE: Careful to use the same files and action in each one. I duplicated
;; the variable in each form to make individual testing easier.
(bench-multi-lexical :times 1 :ensure-equal t
:forms (("normal" (->> (let ((org-ql-cache (ht))
(action-fn (lambda ()
(list (current-buffer)
(point)
(substring-no-properties (org-get-heading t t)))))
(files '("~/org/main.org")))
(org-ql-select files
'(and (not (done))
(or (habit)
(deadline auto)
(scheduled :to today)
(ts-active :on today)
(closed :on today)))
:action action-fn))))
("Testing" (let* ((org-ql-cache (ht))
(files '("~/org/main.org"))
(action-fn (lambda ()
(list (current-buffer)
(point)
(substring-no-properties (org-get-heading t t)))))
(and-queries '(not (done)))
(or-queries '((habit)
(deadline auto)
(scheduled :to today)
(ts-active :on today)
(closed :on today)))
(and-results (org-ql-select files
and-queries
:action action-fn))
(or-results (cl-loop for query in or-queries
append (org-ql-select files
query
:action action-fn))))
(seq-intersection and-results
(->> or-results
-uniq)))))))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| Testing | 1.15 | 0.248376 | 0 | 0 |
| normal | slowest | 0.284897 | 0 | 0 |
;; With caching enabled
(let* ((action-fn (lambda ()
(list (current-buffer)
(point)
(substring-no-properties (org-get-heading t t)))))
(files '("~/org/main.org")))
(bench-multi-lexical :times 1 :ensure-equal t
:forms (("normal" (->> (org-ql-select files
'(and (not (done))
(or (habit)
(deadline auto)
(scheduled :to today)
(ts-active :on today)
(closed :on today)))
:action action-fn)))
("Testing" (let* ((files '("~/org/main.org"))
(and-queries '(not (done)))
(or-queries '((habit)
(deadline auto)
(scheduled :to today)
(ts-active :on today)
(closed :on today)))
(and-results (org-ql-select files
and-queries
:action action-fn))
(or-results (cl-loop for query in or-queries
append (org-ql-select files
query
:action action-fn))))
(seq-intersection and-results
(->> or-results
-uniq)))))))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| normal | 13.72 | 0.002311 | 0 | 0 |
| Testing | slowest | 0.031707 | 0 | 0 |
Using full views:
(let* ((action-fn (lambda ()
(list (current-buffer)
(point)
(substring-no-properties (org-get-heading t t)))))
(files '("~/org/main.org")))
(bench-multi-lexical :times 1
:forms (("normal" (->> (let ((org-ql-cache (ht))
(files '("~/org/main.org")))
(org-ql-search files
'(and (not (done))
(or (habit)
(deadline auto)
(scheduled :to today)
(ts-active :on today)
(closed :on today)))))))
("Testing" (let* ((org-ql-cache (ht))
(files '("~/org/main.org"))
(and-queries '(not (done)))
(or-queries '((habit)
(deadline auto)
(scheduled :to today)
(ts-active :on today)
(closed :on today)))
(and-results (org-ql-select files
and-queries
:action 'element-with-markers))
(or-results (cl-loop for query in or-queries
append (org-ql-select files
query
:action 'element-with-markers)))
(final-results (seq-intersection and-results
(->> or-results
-uniq))))
(org-ql-agenda--agenda nil nil
:entries final-results)
)))))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| normal | 1.74 | 0.534742 | 0 | 0 |
| Testing | slowest | 0.931897 | 0 | 0 |
Just gathering results, but using elements:
(let* ((action-fn 'element-with-markers)
(files '("~/org/main.org")))
;; NOTE: Careful to use the same files and action in each one. I duplicated
;; the variable in each form to make individual testing easier.
(bench-multi-lexical :times 1 :ensure-equal t
:forms (("normal" (->> (let ((org-ql-cache (ht)))
(org-ql-select files
'(and (not (done))
(or (habit)
(deadline auto)
(scheduled :to today)
(ts-active :on today)
(closed :on today)))
:action action-fn))))
("Testing" (let* ((org-ql-cache (ht))
(and-queries '(not (done)))
(or-queries '((habit)
(deadline auto)
(scheduled :to today)
(ts-active :on today)
(closed :on today)))
(and-results (org-ql-select files
and-queries
:action action-fn))
(or-results (cl-loop for query in or-queries
append (org-ql-select files
query
:action action-fn))))
(seq-intersection and-results
(->> or-results
-uniq)))))))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| normal | 2.27 | 0.314218 | 0 | 0 |
| Testing | slowest | 0.714587 | 0 | 0 |
More profiling
[2018-05-10 Thu 15:02] I think these are decent improvements.
(elp-profile 1 nil (org-agenda-ng "~/org/main.org"
(or (habit)
(and (or (date '= (org-today))
(deadline '<=)
(scheduled '<= (org-today)))
(not (apply #'todo org-done-keywords-for-agenda)))
(and (todo "DONE" "CANCELLED")
(closed '= (org-today))))))| Function | Times called | Total time | Average time |
|---|---|---|---|
| mapcar | 164 | 1.5004585290 | 0.0091491373 |
| org-agenda-ng–agenda | 1 | 1.348231247 | 1.348231247 |
| org-agenda-ng–filter-buffer | 1 | 1.1391189879 | 1.1391189879 |
| org-agenda-ng–date-plain-p | 1267 | 0.6198571040 | 0.0004892321 |
| org-entry-get | 3983 | 0.2979337370 | 7.480…e-05 |
| org-is-habit-p | 1365 | 0.2049101109 | 0.0001501172 |
| org–property-local-values | 1365 | 0.1940614150 | 0.0001421695 |
| org-agenda-ng–habit-p | 1272 | 0.1911009179 | 0.0001502365 |
| org-agenda-ng–format-element | 52 | 0.177965411 | 0.0034224117 |
| org-get-property-block | 1365 | 0.1760004519 | 0.0001289380 |
| org-get-tags-at | 52 | 0.1362824969 | 0.0026208172 |
| org-agenda-ng–date-p | 3880 | 0.1351176629 | 3.482…e-05 |
| org-up-heading-safe | 226 | 0.1276747609 | 0.0005649325 |
| re-search-backward | 2028 | 0.1144211070 | 5.642…e-05 |
| org-entry-properties | 2618 | 0.0848660999 | 3.241…e-05 |
| org-agenda-ng–todo-p | 1319 | 0.081952653 | 6.213…e-05 |
| org-get-todo-state | 1319 | 0.0796836810 | 6.041…e-05 |
| re-search-forward | 3754 | 0.0739803739 | 1.970…e-05 |
| org-inlinetask-in-task-p | 1365 | 0.0657829330 | 4.819…e-05 |
| org-agenda-ng–scheduled-p | 1247 | 0.0619497850 | 4.967…e-05 |
Preambles
Not sure if clearing the cache is necessary here, because it seemed to make nearly no difference in the results, but I don’t know why.
(cl-defmacro org-ql-preamble-bench (&key query (file "tests/data.org") (times 10))
`(bench-multi-lets :times ,times :ensure-equal t
:lets (("preamble" ((org-ql-use-preamble t)
(org-ql-cache (ht))))
("no preamble" ((org-ql-use-preamble nil)
(org-ql-cache (ht)))))
:forms ((,(prin1-to-string query) (org-ql-select ,file
',query
:action '(org-get-heading t t))))))closed
(org-ql-preamble-bench :times 1
:file "~/org/inbox.org"
:query (closed))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| preamble: (closed) | 4.80 | 0.086553 | 0 | 0 |
| no preamble: (closed) | slowest | 0.415165 | 0 | 0 |
(org-ql-preamble-bench :times 1
:file "~/org/inbox.org"
:query (closed <= "2019-01-01"))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| preamble: (closed <= “2019-01-01”) | 4.21 | 0.105782 | 0 | 0 |
| no preamble: (closed <= “2019-01-01”) | slowest | 0.445374 | 0 | 0 |
deadline
(org-ql-preamble-bench :times 1
:file "~/org/inbox.org"
:query (deadline))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| preamble: (deadline) | 27.63 | 0.014656 | 0 | 0 |
| no preamble: (deadline) | slowest | 0.404952 | 0 | 0 |
(org-ql-preamble-bench :times 1
:file "~/org/inbox.org"
:query (deadline <= "2019-01-01"))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| preamble: (deadline <= “2019-01-01”) | 27.91 | 0.014606 | 0 | 0 |
| no preamble: (deadline <= “2019-01-01”) | slowest | 0.407682 | 0 | 0 |
habit
(org-ql-preamble-bench :times 1
:file "~/org/inbox.org"
:query (habit))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| preamble: (habit) | 70.09 | 0.016489 | 0 | 0 |
| no preamble: (habit) | slowest | 1.155649 | 0 | 0 |
level
(org-ql-preamble-bench :times 1
:file "~/org/inbox.org"
:query (level 1))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| preamble: (level 1) | 1.34 | 0.562950 | 0 | 0 |
| no preamble: (level 1) | slowest | 0.754050 | 0 | 0 |
property
(org-ql-preamble-bench :times 1
:file "~/org/inbox.org"
:query (property "agenda-group"))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| preamble: (property “agenda-group”) | 70.44 | 0.016571 | 0 | 0 |
| no preamble: (property “agenda-group”) | slowest | 1.167203 | 0 | 0 |
(org-ql-preamble-bench :times 1
:file "~/org/inbox.org"
:query (property "ID"))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| preamble: (property “ID”) | 3.51 | 0.369830 | 0 | 0 |
| no preamble: (property “ID”) | slowest | 1.299684 | 0 | 0 |
(org-ql-preamble-bench :times 1
:file "~/org/inbox.org"
:query (property "agenda-group" "plans"))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| preamble: (property “agenda-group” “plans”) | 72.54 | 0.016862 | 0 | 0 |
| no preamble: (property “agenda-group” “plans”) | slowest | 1.223197 | 0 | 0 |
scheduled
(org-ql-preamble-bench :times 1
:file "~/org/inbox.org"
:query (scheduled))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| preamble: (scheduled) | 4.45 | 0.100968 | 0 | 0 |
| no preamble: (scheduled) | slowest | 0.449321 | 0 | 0 |
(org-ql-preamble-bench :times 1
:file "~/org/inbox.org"
:query (scheduled <= "2019-01-01"))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| preamble: (scheduled <= “2019-01-01”) | 4.13 | 0.111067 | 0 | 0 |
| no preamble: (scheduled <= “2019-01-01”) | slowest | 0.458726 | 0 | 0 |
tags
If tag inheritance is enabled, we have to check tags on every heading. When it’s disabled, we can search directly to headings with the given tags.
(let ((org-use-tag-inheritance t))
(org-ql-preamble-bench :times 1
:file "~/org/inbox.org"
:query (tags "Emacs")))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| no preamble: (tags “Emacs”) | 1.01 | 1.899647 | 0 | 0 |
| preamble: (tags “Emacs”) | slowest | 1.921799 | 0 | 0 |
(let ((org-use-tag-inheritance nil))
(org-ql-preamble-bench :times 1
:file "~/org/inbox.org"
:query (tags "Emacs")))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| preamble: (tags “Emacs”) | 2.08 | 0.274555 | 0 | 0 |
| no preamble: (tags “Emacs”) | slowest | 0.570116 | 0 | 0 |
ts
(org-ql-preamble-bench :times 1
:file "~/org/inbox.org"
:query (ts))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| preamble: (ts) | 1.13 | 0.475646 | 0 | 0 |
| no preamble: (ts) | slowest | 0.535950 | 0 | 0 |
(org-ql-preamble-bench :times 1
:file "~/org/inbox.org"
:query (ts :from "2019-01-01"))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| no preamble: (ts :from “2019-01-01”) | 1.11 | 0.537445 | 0 | 0 |
| preamble: (ts :from “2019-01-01”) | slowest | 0.594534 | 0 | 0 |
(org-ql-preamble-bench :times 1
:file "~/org/inbox.org"
:query (ts :from "2017-01-01"))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| no preamble: (ts :from “2017-01-01”) | 1.13 | 0.526891 | 0 | 0 |
| preamble: (ts :from “2017-01-01”) | slowest | 0.594360 | 0 | 0 |
Not sure why that one is slower with preamble.
(org-ql-preamble-bench :times 10
:query (ts :from "2017-01-01"))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| no preamble: (ts :from “2017-01-01”) | 1.04 | 0.025688 | 0 | 0 |
| preamble: (ts :from “2017-01-01”) | slowest | 0.026642 | 0 | 0 |
(org-ql-preamble-bench :times 1
:file "~/org/inbox.org"
:query (ts :to "2010-01-01"))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| no preamble: (ts :to “2010-01-01”) | 1.10 | 0.538603 | 0 | 0 |
| preamble: (ts :to “2010-01-01”) | slowest | 0.593466 | 0 | 0 |
ts-active
(org-ql-preamble-bench :times 1
:file "~/org/inbox.org"
:query (ts-a))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| preamble: (ts-a) | 4.77 | 0.071489 | 0 | 0 |
| no preamble: (ts-a) | slowest | 0.340896 | 0 | 0 |
(org-ql-preamble-bench :times 1
:file "~/org/inbox.org"
:query (ts-a :from "2017-07-06"))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| preamble: (ts-a :from “2017-07-06”) | 1.78 | 0.188369 | 0 | 0 |
| no preamble: (ts-a :from “2017-07-06”) | slowest | 0.335975 | 0 | 0 |
(org-ql-preamble-bench :times 1
:file "~/org/inbox.org"
:query (ts-a :to "2017-07-06"))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| preamble: (ts-a :to “2017-07-06”) | 4.64 | 0.075307 | 0 | 0 |
| no preamble: (ts-a :to “2017-07-06”) | slowest | 0.349445 | 0 | 0 |
(org-ql-preamble-bench :times 1
:file "~/org/inbox.org"
:query (ts-a :on "2017-07-06"))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| preamble: (ts-a :on “2017-07-06”) | 4.33 | 0.076075 | 0 | 0 |
| no preamble: (ts-a :on “2017-07-06”) | slowest | 0.329106 | 0 | 0 |
ts-inactive
(org-ql-preamble-bench :times 1
:file "~/org/inbox.org"
:query (ts-i))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| preamble: (ts-i) | 1.21 | 0.459152 | 0 | 0 |
| no preamble: (ts-i) | slowest | 0.555632 | 0 | 0 |
(org-ql-preamble-bench :times 1
:file "~/org/inbox.org"
:query (ts-i :from "2019-07-06"))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| no preamble: (ts-i :from “2019-07-06”) | 1.09 | 0.531976 | 0 | 0 |
| preamble: (ts-i :from “2019-07-06”) | slowest | 0.579745 | 0 | 0 |
(org-ql-preamble-bench :times 1
:file "~/org/inbox.org"
:query (ts-i :to "2019-07-06"))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| no preamble: (ts-i :to “2019-07-06”) | 1.34 | 0.553428 | 0 | 0 |
| preamble: (ts-i :to “2019-07-06”) | slowest | 0.743881 | 0 | 0 |
Profiling flet across all agenda files
With flet
(elp-profile 5 (org-agenda-ng--agenda
:files org-agenda-files
:pred (lambda ()
(and (todo)
(or (date :deadline '<= (org-today))
(date :scheduled '<= (org-today)))
(not (apply #'todo org-done-keywords-for-agenda))))))
Without flet
(elp-profile 5 (org-agenda-ng--agenda
:files org-agenda-files
:pred (lambda ()
(and (org-agenda-ng--todo-p)
(or (org-agenda-ng--date-p :deadline '<= (org-today))
(org-agenda-ng--date-p :scheduled '<= (org-today)))
(not (apply #'org-agenda-ng--todo-p org-done-keywords-for-agenda))))))
Profiling flet on a single file
This shows that the difference between them, if any, is so small as to be irrelevant. The convenience and clarity are a big win.
With flet
(elp-profile 5 (org-agenda-ng--agenda
:files "~/org/main.org"
:pred (lambda ()
(and (todo)
(or (date :deadline '<= (org-today))
(date :scheduled '<= (org-today)))
(not (apply #'todo org-done-keywords-for-agenda))))))
Without flet
(elp-profile 5 (org-agenda-ng--agenda
:files "~/org/main.org"
:pred (lambda ()
(and (org-agenda-ng--todo-p)
(or (org-agenda-ng--date-p :deadline '<= (org-today))
(org-agenda-ng--date-p :scheduled '<= (org-today)))
(not (apply #'org-agenda-ng--todo-p org-done-keywords-for-agenda))))))
Profiling org-trust-scanner-tags
[2018-05-10 Thu 12:59] Turned on org-trust-scanner-tags, going to try profiling again:
;; (elp-profile 1 nil (org-agenda-ng "~/src/emacs/org-super-agenda/test/test.org"
;; (tags "world")))
(elp-profile 10 nil (org-agenda-ng org-agenda-files
(tags "Emacs")))| Function | Times called | Total time | Average time |
|---|---|---|---|
| org-agenda-ng–agenda | 10 | 44.092598282 | 4.4092598282 |
| mapcar | 282 | 40.234516707 | 0.1426755911 |
| org-agenda-ng–filter-buffer | 80 | 26.895492471 | 0.3361936558 |
| org-element-headline-parser | 3980 | 10.387614362 | 0.0026099533 |
| org-agenda-finalize-entries | 10 | 9.194458252 | 0.9194458252 |
| org-agenda-ng–tags-p | 70250 | 8.1897379849 | 0.0001165799 |
| org-agenda-ng–format-element | 3980 | 6.5944325679 | 0.0016568926 |
| outline-next-heading | 70320 | 6.1190180490 | 8.701…e-05 |
| re-search-forward | 97050 | 5.8706467829 | 6.049…e-05 |
| org-get-tags-at | 74230 | 5.4078158059 | 7.285…e-05 |
| org-super-agenda–filter-finalize-entries | 10 | 5.2320123400 | 0.5232012340 |
| org-super-agenda–group-items | 10 | 5.1260959210 | 0.5126095921 |
| org-super-agenda–group-dispatch | 130 | 5.119333624 | 0.0393794894 |
| sort | 20 | 3.8204368569 | 0.1910218428 |
| org-element–parse-objects | 6180 | 3.5386578929 | 0.0005725983 |
| org-is-habit-p | 5970 | 3.2497755920 | 0.0005443510 |
| org-entry-get | 5970 | 3.2347964049 | 0.0005418419 |
| org–property-local-values | 5970 | 3.1796357319 | 0.0005326023 |
| org-get-property-block | 5970 | 3.0767919680 | 0.0005153755 |
| org-entries-lessp | 20020 | 2.6563960079 | 0.0001326871 |
Now trying again without it:
;; (elp-profile 1 nil (org-agenda-ng "~/src/emacs/org-super-agenda/test/test.org"
;; (tags "world")))
(elp-profile 10 nil (org-agenda-ng org-agenda-files
(tags "Emacs")))| Function | Times called | Total time | Average time |
|---|---|---|---|
| mapcar | 1791 | 57.096304538 | 0.0318795670 |
| org-agenda-ng–agenda | 10 | 54.232133506 | 5.4232133505 |
| org-agenda-ng–filter-buffer | 80 | 30.065167040 | 0.3758145880 |
| org-get-tags-at | 74230 | 13.840202495 | 0.0001864502 |
| org-agenda-ng–format-element | 3980 | 13.429297797 | 0.0033741954 |
| org-element-headline-parser | 3980 | 12.771776652 | 0.0032089891 |
| org-agenda-finalize-entries | 10 | 9.1439433990 | 0.9143943399 |
| org-agenda-ng–tags-p | 70250 | 9.0249653730 | 0.0001284692 |
| org-super-agenda–filter-finalize-entries | 10 | 7.300515859 | 0.7300515859 |
| outline-next-heading | 70320 | 7.2384435649 | 0.0001029357 |
| org-super-agenda–group-items | 10 | 4.918585855 | 0.4918585855 |
| org-super-agenda–group-dispatch | 130 | 4.9125893509 | 0.0377891488 |
| re-search-forward | 101020 | 4.6294823850 | 4.582…e-05 |
| org-up-heading-safe | 7370 | 4.4629885620 | 0.0006055615 |
| org-is-habit-p | 5960 | 4.2772351910 | 0.0007176569 |
| org-entry-get | 5960 | 4.2595350800 | 0.0007146870 |
| org-super-agenda–group-tag | 50 | 3.8942044929 | 0.0778840898 |
| re-search-backward | 26150 | 3.3660083490 | 0.0001287192 |
| org–property-local-values | 5960 | 3.1793476329 | 0.0005334475 |
| org-get-property-block | 5960 | 3.0662425979 | 0.0005144702 |
Wow, using org-trust-scanner-tags saves a lot of time.
Profiling position-based
Macro
(defmacro elp-profile (times &rest body)
(declare (indent defun))
`(let ((prefixes '("org-" "string-" "s-" "buffer-" "append" "delq" "map"
"list" "car" "save-" "outline-" "delete-dups"
"sort" "line-" "nth" "concat" "char-to-string"
"rx-" "goto-" "when" "search-" "re-"))
output)
(dolist (prefix prefixes)
(elp-instrument-package prefix))
(dotimes (x ,times)
,@body)
(elp-results)
(elp-restore-all)
(point-min)
(forward-line 20)
(delete-region (point) (point-max))
(setq output (buffer-substring-no-properties (point-min) (point-max)))
(kill-buffer)
(delete-window)
output))ng-flet
(elp-profile 5 (org-agenda-ng--test-agenda-today))
ng-funcall
(elp-profile 5 (org-agenda-ng--test-agenda-today))
orig
Make sure to kill any existing agenda buffers first.
(elp-profile 1 (org-agenda-list nil nil 'week))
Profiling tags matching
ng
(elp-profile 1 nil
(org-agenda-ng "~/org/main.org"
(tags "computer")))| Function | Times called | Total time | Average time |
|---|---|---|---|
| mapcar | 4217 | 12.612716455 | 0.0029909216 |
| org-agenda-ng–agenda | 1 | 9.721410651 | 9.721410651 |
| org-get-tags-at | 1845 | 7.4793860389 | 0.0040538677 |
| org-up-heading-safe | 9361 | 6.4622674019 | 0.0006903394 |
| re-search-backward | 25001 | 5.3399866239 | 0.0002135909 |
| org-agenda-ng–filter-buffer | 1 | 4.874598854 | 4.874598854 |
| org-agenda-ng–tags-p | 1238 | 4.8067623430 | 0.0038826836 |
| org-agenda-ng–format-element | 607 | 3.6325626609 | 0.0059844524 |
| org-outline-level | 17484 | 1.0298924459 | 5.890…e-05 |
| org-add-props | 2074 | 0.8305549259 | 0.0004004604 |
| org-element-headline-parser | 607 | 0.2092664829 | 0.0003447553 |
| org-back-to-heading | 11813 | 0.1252112960 | 1.059…e-05 |
| outline-back-to-heading | 11813 | 0.1100693780 | 9.317…e-06 |
| org-end-of-subtree | 607 | 0.0721986340 | 0.0001189433 |
| outline-on-heading-p | 11813 | 0.0675261030 | 5.716…e-06 |
| outline-next-heading | 1239 | 0.0627980999 | 5.068…e-05 |
| re-search-forward | 3273 | 0.0612446620 | 1.871…e-05 |
| org-agenda-finalize-entries | 1 | 0.041846274 | 0.041846274 |
| buffer-substring-no-properties | 6329 | 0.0308716979 | 4.877…e-06 |
| line-end-position | 903 | 0.0280484950 | 3.106…e-05 |
ng without inheritance
(elp-profile 1 nil
(org-agenda-ng "~/org/main.org"
(tags "computer")))| Function | Times called | Total time | Average time |
|---|---|---|---|
| mapcar | 4217 | 12.580246839 | 0.0029832219 |
| org-agenda-ng–agenda | 1 | 8.777776059 | 8.777776059 |
| org-get-tags-at | 1845 | 8.2853503299 | 0.0044907047 |
| org-up-heading-safe | 9361 | 7.2710981889 | 0.0007767437 |
| re-search-backward | 25001 | 5.3360082060 | 0.0002134317 |
| org-agenda-ng–filter-buffer | 1 | 4.865602689 | 4.865602689 |
| org-agenda-ng–tags-p | 1238 | 4.7983754310 | 0.0038759090 |
| org-agenda-ng–format-element | 607 | 3.6273825100 | 0.0059759184 |
| org-outline-level | 17484 | 1.0284417919 | 5.882…e-05 |
| org-back-to-heading | 11813 | 0.9390534479 | 7.949…e-05 |
| org-split-string | 4940 | 0.833825087 | 0.0001687905 |
| string-match | 9102 | 0.8231629100 | 9.043…e-05 |
| org-element-headline-parser | 607 | 0.2034305819 | 0.0003351409 |
| outline-back-to-heading | 11813 | 0.1096120189 | 9.278…e-06 |
| org-end-of-subtree | 607 | 0.0710802559 | 0.0001171009 |
| outline-on-heading-p | 11813 | 0.0670029359 | 5.671…e-06 |
| outline-next-heading | 1239 | 0.0622323519 | 5.022…e-05 |
| re-search-forward | 3273 | 0.0603102519 | 1.842…e-05 |
| org-agenda-finalize-entries | 1 | 0.037286496 | 0.037286496 |
| buffer-substring-no-properties | 6329 | 0.0285818689 | 4.516…e-06 |
original
(elp-profile 1 nil
(with-current-buffer "main.org"
(org-tags-view nil "computer")))| Function | Times called | Total time | Average time |
|---|---|---|---|
| org-tags-view | 1 | 2.620578129 | 2.620578129 |
| org-scan-tags | 1 | 1.448883817 | 1.448883817 |
| org-agenda-format-item | 607 | 0.9273893060 | 0.0015278242 |
| org-add-props | 2042 | 0.8877267209 | 0.0004347339 |
| org-agenda-finalize | 1 | 0.144506782 | 0.144506782 |
| re-search-forward | 2154 | 0.1367046650 | 6.346…e-05 |
| string-match | 8742 | 0.1002517259 | 1.146…e-05 |
| org-get-priority | 607 | 0.0961996220 | 0.0001584837 |
| org-agenda-align-tags | 1 | 0.095166495 | 0.095166495 |
| org-agenda-prepare | 1 | 0.081724472 | 0.081724472 |
| org-outline-level | 1246 | 0.0771033170 | 6.188…e-05 |
| org-agenda-finalize-entries | 1 | 0.071707404 | 0.071707404 |
| org-agenda-prepare-buffers | 1 | 0.057903921 | 0.057903921 |
| org-get-heading | 607 | 0.0517784369 | 8.530…e-05 |
| mapcar | 3738 | 0.0418641110 | 1.119…e-05 |
| org-agenda-highlight-todo | 607 | 0.0273123070 | 4.499…e-05 |
| mapconcat | 609 | 0.024743305 | 4.062…e-05 |
| sort | 2 | 0.02117069 | 0.010585345 |
| org-activate-plain-links | 132 | 0.0203558980 | 0.0001542113 |
| org-activate-bracket-links | 78 | 0.0198589680 | 0.0002546021 |
Using org-element-parse-buffer
This basically works, as a very basic kind of agenda view, but we can already see that it’s much slower (at least, for single-day views) because org-element-parse-buffer is slow compared to the agenda code.
[2018-05-10 Thu 15:03] Note: This is the old, much slower code that used org-element-parse-buffer.
Macro
(defmacro elp-profile (times prefixes &rest body)
(declare (indent defun))
(let ((prefixes (append '(org- string- s- buffer- append delq map
list car save- outline- delete-dups
sort line- nth concat char-to-string
rx- goto- when search- re-)
prefixes)))
`(let (output)
(dolist (prefix ',prefixes)
(elp-instrument-package (symbol-name prefix)))
(dotimes (x ,times)
,@body)
(elp-results)
(elp-restore-all)
(point-min)
(forward-line 20)
(delete-region (point) (point-max))
(setq output (buffer-substring-no-properties (point-min) (point-max)))
(kill-buffer)
(delete-window)
(let ((rows (s-lines output)))
(append (list (list "Function" "Times called" "Total time" "Average time")
'hline)
(cl-loop for row in rows
collect (s-split (rx (1+ space)) row 'omit-nulls)))))))[2018-05-09 Wed 17:31] Note: I seem to have misplaced the org-agenda-ng--test-agenda-today function I used in these tests.
ng
(elp-profile 1 (org-agenda-ng--test-agenda-today))
orig
Make sure to kill any existing agenda buffers first.
(elp-profile 1 (org-agenda-list nil nil 'week))
Profile org-element-map
(elp-profile 1 (with-current-buffer (find-buffer-visiting "~/org/main.org")
(org-element-parse-buffer 'headline)))
with/without ts.el
[2019-08-11 Sun 15:39] These results seem to show a minor performance improvement by using ts, and the code is simpler.
;; (require 'ts)
(org-ql--defpred ts-ts (&key from to _on)
;; The underscore before `on' prevents "unused lexical variable" warnings, because we
;; pre-process that argument in a macro before this function is called.
"Return non-nil if current entry has a timestamp in given period.
If no arguments are specified, return non-nil if entry has any
timestamp.
If FROM, return non-nil if entry has a timestamp on or after
FROM.
If TO, return non-nil if entry has a timestamp on or before TO.
If ON, return non-nil if entry has a timestamp on date ON.
FROM, TO, and ON should be strings parseable by
`parse-time-string' but may omit the time value."
;; TODO: DRY this with the clocked predicate.
;; NOTE: FROM and TO are actually expected to be Unix timestamps. The docstring is written
;; for end users, for which the arguments are pre-processed by `org-ql-select'.
;; FIXME: This assumes every "clocked" entry is a range. Unclosed clock entries are not handled.
(cl-macrolet ((next-timestamp ()
`(when (re-search-forward org-element--timestamp-regexp end-pos t)
(ts-parse-org (match-string 0))))
(test-timestamps (pred-form)
`(cl-loop for next-ts = (next-timestamp)
while next-ts
thereis ,pred-form)))
(save-excursion
(let ((end-pos (org-entry-end-position)))
(cond ((not (or from to)) (re-search-forward org-element--timestamp-regexp end-pos t))
((and from to) (test-timestamps (and (ts<= from next-ts)
(ts<= next-ts to))))
(from (test-timestamps (ts<= from next-ts)))
(to (test-timestamps (ts<= next-ts to))))))))
:from
(bench-multi-lexical :times 1 :ensure-equal t
:forms (("old ts" (org-ql "~/org/inbox.org"
(ts :from "2017-01-01")))
("ts.el ts" (org-ql "~/org/inbox.org"
(ts-ts :from "2017-01-01")))))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| ts.el ts | 1.32 | 1.299966 | 0 | 0 |
| old ts | slowest | 1.713027 | 0 | 0 |
:on
(bench-multi-lexical :times 1 :ensure-equal t
:forms (("old ts" (org-ql "~/org/inbox.org"
(ts :on "2019-05-14")))
("ts.el ts" (org-ql "~/org/inbox.org"
(ts-ts :on "2019-05-14")))))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| ts.el ts | 1.17 | 0.557281 | 0 | 0 |
| old ts | slowest | 0.652149 | 0 | 0 |
:to
(bench-multi-lexical :times 1 :ensure-equal t
:forms (("old ts" (org-ql "~/org/inbox.org"
(ts :to "2019-01-01")))
("ts.el ts" (org-ql "~/org/inbox.org"
(ts-ts :to "2019-01-01")))))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| ts.el ts | 1.01 | 1.300084 | 0 | 0 |
| old ts | slowest | 1.312208 | 0 | 0 |
Without timestamp argument
(bench-multi-lexical :times 1 :ensure-equal t
:forms (("old ts" (org-ql "~/org/inbox.org"
(ts)))
("ts.el ts" (org-ql "~/org/inbox.org"
(ts-ts)))))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| ts.el ts | 1.14 | 2.251801 | 0 | 0 |
| old ts | slowest | 2.560280 | 0 | 0 |
(bench-multi-lexical :times 20 :ensure-equal t
:forms (("old ts" (org-ql "~/src/emacs/org-ql/tests/data.org"
(ts)))
("ts.el ts" (org-ql "~/src/emacs/org-ql/tests/data.org"
(ts-ts)))))| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
|---|---|---|---|---|
| ts.el ts | 1.05 | 0.103714 | 0 | 0 |
| old ts | slowest | 0.108663 | 0 | 0 |
References
- GitHub - ndwarshuis/org-sql: SQL backend for Emacs Org-Mode
- John Kitchin on rewriting the Org agenda code
- Nicolas Goaziou’s org-element cache implementation
- Uniform Structured Syntax, Metaprogramming and Run-time Compilation
GitHub - ndwarshuis/org-sql: SQL backend for Emacs Org-Mode
[2020-01-04 Sat 09:03]
John Kitchin on rewriting the Org agenda code
:archive.is: http://archive.is/33R9M[2019-10-28 Mon 08:28] Originally from this entry in my notes.
From: John Kitchin <jkitchin@andrew.cmu.edu> Subject: Re: [Orgmode] Slow speed of week and month views Newsgroups: gmane.emacs.orgmode To: Karl Voit <news1142@karl-voit.at> Cc: “emacs-orgmode@gnu.org” <emacs-orgmode@gnu.org> Date: Sat, 5 Aug 2017 18:17:09 -0400 (4 hours, 11 minutes, 38 seconds ago)
I can think of two possibilities for a future approach (besides a deep dive on profiling the current elisp to improve the speed there). They both involve some substantial coding though, and would probably add dependencies. I am curious what anyone things about these, or if there are other ideas.
One is to use the new dynamic module capability to write an org parser in C, or a dedicated agenda function, which would presumably be faster than in elisp. This seems hard, and for me would certainly be a multiyear project I am sure! The downside of this is the need to compile the module. I don’t know how easy it would be to make this work across platforms with the relatively easy install org-mode currently has. This could have a side benefit though of a c-lib that could be used by others to expand where org-mode is used.
The other way that might work is to rely more heavily on a cached version of the files, perhaps in a different format than elisp, that is faster to work with. The approach I have explored in this is to index org files into a sqlite database. The idea then would be to generate the agenda from a sql query. I use something like this already to “find stuff in orgmode anywhere”. One of the reasons I wrote this is the org-agenda list of files isn’t practical for me because my files are so scattered on my file system. I had a need to be able to find TODOs in research projects in a pretty wide range of locations.
The code I use is at https://github.com/jkitchin/scimax/blob/master/org-db.el, and from one database I can find headlines, contacts, locations, TODO headlines across my file system, all the files that contain a particular link, and my own recent org files. This approach relies on emacsql, and a set of hook functions to update the database whenever a file is changed. It is not robust, e.g. the file could be out of sync with the db if it is modified outside emacs, but this works well enough for me so far. Updated files get reindexed whenever emacs is idle. It was a compromise on walking the file system all the time or daily, or trying to use inotify and you can always run a command to prune/sync all the files any time you want.
sqlite is ok, but with emacsql you cannot put strings in it directly (at least when I wrote the org-db code), which has limited it for full-text search so far. Also with text, the db got up to about 0.5 GB in size, and started slowing down. So it doesn’t have text in it for now. It has all the other limitations of sqlite too, limited support for locking, single process….
I am moderately motivated to switch from sqlite to MongoDB, but the support for Mongo in emacs is pretty crummy (I tried writing a few traditional interfaces, but the performance was not that good, and limited since Mongo uses bson, and it is just not the same as json!). Why Mongo? Mostly because the Mongo query language is basically json and easy to generate in Emacs, unlike sql. Also, it is flexible and easy to adapt to new things, e.g. indexing src-blocks or tables or whatever org-element you want. (And I want to use Mongo for something else too ;). Obviously these all add dependencies, and might not be suitable for the core org-mode distribution. But I do think it is important to think about ways to scale org-mode while maintaining compatibility with the core.
The main point of the database was to get a query language, persistence and good performance. I have also used caches to speed up using bibtex files, and my org-contacts with reasonable performance. These have been all elisp, with no additional dependencies. Maybe one could do something similar to keep an agenda cache that is persistent and updated via hook functions.
Thoughts?
John
Nicolas Goaziou’s org-element cache implementation
[2020-01-04 Sat 08:31] https://code.orgmode.org/bzg/org-mode/src/master/lisp/org-element.el#L4817 I guess I haven’t been keeping up, because I’m not sure when exactly he made or committed this, or what his plans for it are. It’s currently disabled by default.
Uniform Structured Syntax, Metaprogramming and Run-time Compilation
Testing
[2020-01-08 Wed 07:15] Subtree moved from tests/data.org.
(cl-defun ap/org-tweak-timestamps (&key (offset 0) epoch-ts)
"Advance all timestamps in the current buffer as if the earliest one was on today.
OFFSET changes which timestamp (in chronological order) is set to
today. Or, if EPOCH-TS is non-nil, use it as the new zero-point
for today."
(let* ((tss (->> (org-with-wide-buffer
(goto-char (point-min))
(cl-loop while (re-search-forward org-ts-regexp-both nil t)
collect (ts-parse-org (match-string 0))))
(-sort #'ts<)))
(epoch-ts (if epoch-ts
(ts-parse-org epoch-ts)
(nth offset tss)))
(difference-secs (ts-diff (ts-now) epoch-ts))
(days (floor (/ difference-secs 86400))))
(org-with-wide-buffer
(goto-char (point-min))
(while (re-search-forward org-ts-regexp-both nil t)
(let* ((ts (ts-parse-org (match-string 0)))
(timed-p (string-match-p (rx (repeat 2 digit) ":" (repeat 2 digit) (or ">" "]")) (match-string 0)))
(type (cond ((string-prefix-p "<" (match-string 0)) 'active)
((string-prefix-p "[" (match-string 0)) 'inactive)
(t (error "Unknown ts type"))))
(brackets (cl-ecase type
('active (cons "<" ">"))
('inactive (cons "[" "]"))))
(format-string (concat (car brackets)
(if timed-p
"%Y-%m-%d %a %H:%M"
"%Y-%m-%d %a")
(cdr brackets)))
(new-ts (ts-adjust 'day days ts))
(new-ts-string (ts-format format-string new-ts)))
(replace-match new-ts-string t t nil 0))))
(message "Tweaked from epoch: %s" (ts-format epoch-ts))))(org-time-string-to-absolute (org-entry-get (point) "SCHEDULED"));; Setup code
(require 'org-super-agenda)
(org-super-agenda-mode 1)
(require 'org-habit)
(setq org-todo-keywords
'((sequence "TODO(t!)" "TODAY(a!)" "NEXT(n!)" "STARTED(s!)" "IN-PROGRESS(p!)" "UNDERWAY(u!)" "WAITING(w@)" "SOMEDAY(o!)" "MAYBE(m!)" "|" "DONE(d@)" "CANCELED(c@)")
(sequence "CHECK(k!)" "|" "DONE(d@)")
(sequence "TO-READ(r!)" "READING(R!)" "|" "HAVE-READ(d@)")
(sequence "TO-WATCH(!)" "WATCHING(!)" "SEEN(!)")))
(with-current-buffer "test.org" (revert-buffer))
(defmacro with-org-today-date (date &rest body)
"Run BODY with the `org-today' function set to return simply DATE.
DATE should be a date-time string (both date and time must be included)."
(declare (indent defun))
`(let ((day (date-to-day ,date))
(orig (symbol-function 'org-today)))
(unwind-protect
(progn
(fset 'org-today (lambda () day))
,@body)
(fset 'org-today orig))))(defun diary-sunrise ()
(let ((dss (diary-sunrise-sunset)))
(with-temp-buffer
(insert dss)
(goto-char (point-min))
(search-forward ",")
(buffer-substring (point-min) (match-beginning 0)))))
(defun diary-sunset ()
(let ((dss (diary-sunrise-sunset))
start end)
(with-temp-buffer
(insert dss)
(goto-char (point-min))
(search-forward ", ")
(setq start (match-end 0))
(search-forward " at")
(setq end (match-beginning 0))
(goto-char start)
(capitalize-word 1)
(buffer-substring start end))))Note: Removing tests from here as they’re added to test.el.
(org-super-agenda--test-with-org-today-date "2017-07-05 00:00"
(let ((org-agenda-files (list "~/src/emacs/org-super-agenda/test/test.org"))
(org-agenda-span 'day)
(org-super-agenda-groups
'((:name "Time grid items in all-uppercase with RosyBrown1 foreground"
:time-grid t
:transformer (--> it
(upcase it)
(propertize it 'face '(:foreground "RosyBrown1"))))
(:name "Priority >= C items underlined, on black background"
:face (:background "black" :underline t)
:not (:priority>= "C")
:order 100))))
(org-agenda nil "a")))
(org-super-agenda--test-with-org-today-date "2017-07-05 00:00"
(let ((org-agenda-files (list "~/src/emacs/org-super-agenda/test/test.org"))
(org-agenda-span 'day)
(org-super-agenda-groups
'((:name none
:time-grid t)
(:name "Should be all-uppercase RosyBrown1 on black"
:face (:background "black" :foreground "RosyBrown1")
:transformer #'upcase
:not (:priority>= "C")
:order 100))))
(org-agenda nil "a")))
(with-org-today-date "2017-07-05 00:00"
(let ((org-agenda-files (list "~/src/org-super-agenda/test/test.org"))
(org-agenda-span 'day)
(org-super-agenda-groups
'((:name none
:time-grid t)
(:name none
:not (:priority>= "C")
:order 100))))
(org-agenda nil "a")))
(with-org-today-date "2017-07-05 00:00"
(let ((org-agenda-files (list "~/src/org-super-agenda/test/test.org"))
(org-agenda-span 'day)
(org-super-agenda-groups
'((:name "Items with child TODOs"
:children todo))))
(org-agenda nil "a")))
(with-org-today-date "2017-07-05 00:00"
(let ((org-agenda-files (list "~/src/org-super-agenda/test/test.org"))
(org-agenda-span 'day)
(org-super-agenda-groups
'((:name "Items with child TODOs"
:children "CHECK"))))
(org-agenda nil "a")))
(with-org-today-date "2017-07-05 00:00"
(let ((org-agenda-files (list "~/src/org-super-agenda/test/test.org"))
(org-agenda-span 'day)
(org-agenda-custom-commands
'(("u" "Super view"
((agenda "" ((org-super-agenda-groups
'((:name "Today"
:time-grid t
:scheduled today
:deadline today)))))
(todo "" ((org-super-agenda-groups
'((:name "Projects"
:children t)
(:discard (:anything t)))))))))))
(org-agenda nil "u")))
(with-org-today-date "2017-07-05 00:00"
(let ((org-agenda-files (list "~/src/org-super-agenda/test/test.org"))
(org-agenda-span 'day)
(org-agenda-custom-commands
'(("u" "Super view"
((agenda "" ((org-super-agenda-groups
'((:name "Today"
:time-grid today)))))
(todo "" ((org-super-agenda-groups
'((:name "Projects"
:children t)
(:discard (:anything t)))))))))))
(org-agenda nil "u")))
(with-org-today-date "2017-07-05 00:00"
(let ((org-agenda-files (list "~/src/org-super-agenda/test/test.org"))
(org-agenda-span 'day)
(org-agenda-custom-commands
'(("u" "Super view"
((agenda "" ((org-super-agenda-groups
'((:name "Schedule"
:time-grid t
:date today)
(:name "Due today"
:deadline today)
(:name "Due soon"
:deadline t)))))
(todo "" ((org-agenda-overriding-header "")
(org-super-agenda-groups
'((:name "Projects"
:children t)
(:discard (:anything t)))))))))))
(org-agenda nil "u")))
(with-org-today-date "2017-07-05 00:00"
(let ((org-agenda-files (list "~/src/org-super-agenda/test/test.org"))
(org-agenda-span 'day)
(org-super-agenda-groups
'((:scheduled (before "2017-07-06")))))
(org-agenda nil "a"))) (with-org-today-date "2017-07-05 00:00"
(let ((org-super-agenda-groups
'((:todo "WAITING")))
(org-agenda-files (list "~/src/org-super-agenda/test/test.org")))
(org-todo-list)))
(with-org-today-date "2017-07-05 00:00"
(let ((org-super-agenda-groups
'((:todo "SOMEDAY")))
(org-agenda-files (list "~/src/org-super-agenda/test/test.org")))
(org-tags-view nil "Emacs")))
(with-org-today-date "2017-07-05 00:00"
(let ((org-super-agenda-groups
'((:todo "CHECK")))
(org-agenda-files (list "~/src/org-super-agenda/test/test.org")))
;; org-search-view doesn't seem to set the todo-state property, so the matcher doesn't work
(org-search-view nil "Emacs")))
(with-org-today-date "2017-07-05 00:00"
(let ((org-super-agenda-groups
'((:regexp ("moon" "mars"))))
(org-agenda-files (list "~/src/org-super-agenda/test/test.org")))
(org-search-view nil "space")))
(with-org-today-date "2017-07-05 00:00"
(let ((org-super-agenda-groups
'((:todo "SOMEDAY")))
(org-agenda-files (list "~/src/org-super-agenda/test/test.org")))
(org-agenda-list nil nil 'day)))
Agenda censoring
For sharing screenshots of the agenda without revealing private data.
(defun org-agenda-sharpie ()
"Censor the text of items in the agenda."
(interactive)
(let (regexp old-heading new-heading properties)
;; Save face properties of line in agenda to reapply to changed text
(setq properties (text-properties-at (point)))
;; Go to source buffer
(org-with-point-at (org-find-text-property-in-string 'org-marker
(buffer-substring (line-beginning-position)
(line-end-position)))
;; Save old heading text and ask for new text
(line-beginning-position)
(unless (org-at-heading-p)
;; Not sure if necessary
(org-back-to-heading))
(setq old-heading (when (looking-at org-complex-heading-regexp)
(match-string 4))))
(setq new-heading (read-from-minibuffer "Overwrite visible heading with: "))
(add-text-properties 0 (length new-heading) properties new-heading)
;; Back to agenda buffer
(save-excursion
(when (and old-heading new-heading)
;; Replace agenda text
(let ((inhibit-read-only t))
(goto-char (line-beginning-position))
(when (search-forward old-heading (line-end-position))
(replace-match new-heading 'fixedcase 'literal)))))))Agenda examining
This helps a lot.
(defun data-debug-show-string-with-properties (s)
(with-current-buffer (get-buffer-create "argh")
(erase-buffer)
(print s (current-buffer))
;; Convert string reader representations to plain lists that can be set
(cl-loop for (match replace) in '(("#(" "'(")
("#<" "'(")
(">" ")"))
do (progn
(goto-char (point-min))
(while (search-forward match nil 'noerror)
(replace-match replace 'fixedcase 'literal))))
;; Surround content in a list which `argh' is set to, then eval
;; the buffer to do it
(goto-char (point-min))
(insert "(setq argh (list '")
(delete-forward-char 2)
(goto-char (point-max))
(insert "))")
;; Okay, sure, eval'ing the buffer is dangerous and bad and wrong.
;; But this is the only way I can find to make this work. (Maybe
;; `text-properties-at' could be used to get actual lists...)
(eval-buffer)
(data-debug-show-stuff argh "argh")
;; (switch-to-buffer (current-buffer))
))
(defun data-debug-show-current-line-with-properties ()
(interactive)
(data-debug-show-string-with-properties (buffer-substring (line-beginning-position) (line-end-position))))
(with-current-buffer "*Org Agenda*"
(data-debug-show-string-with-properties (seq-subseq (split-string (buffer-string) "\n")
0 5)))Auto categories
(let ((org-super-agenda-groups
'((:auto-category t))))
(org-agenda-list nil nil 'day))Auto grouping
(with-org-today-date "2017-07-05 00:00"
(let ((org-super-agenda-groups
'((:auto-group t)))
(org-agenda-files (list "~/src/org-super-agenda/test/test.org")))
(org-agenda-list nil nil 'day)))Date
(with-org-today-date "2017-07-05 00:00"
(-let* ((org-agenda-files (list "~/src/org-super-agenda/test/test.org"))
(org-agenda-span 'day)
((sec minute hour day month year dow dst utcoff) (list 0 0 0 5 7 2017 3 t nil))
(last-day-of-month
;; A hack that seems to work fine
(1+ (calendar-last-day-of-month month year)))
(target-date (format "%d-%02d-%02d" year month last-day-of-month))
(org-super-agenda-groups
`((:deadline (before ,target-date))
(:discard (:anything t)))))
(org-todo-list)))
(with-org-today-date "2017-07-05 00:00"
(-let* ((org-agenda-files (list "~/src/org-super-agenda/test/test.org"))
(org-agenda-span 'day)
((sec minute hour day month year dow dst utcoff) (list 0 0 0 5 7 2017 3 t nil))
(last-day-of-month (calendar-last-day-of-month month year))
(target-date (format "%d-%02d-%02d" year month last-day-of-month))
(org-super-agenda-groups
`((:deadline (before ,target-date))
(:discard (:anything t)))))
(org-todo-list)))Effort
(with-org-today-date "2017-07-05 00:00"
(let ((org-agenda-files (list "~/src/org-super-agenda/test/test.org"))
(org-super-agenda-groups
'((:effort< "0:06"))))
(org-agenda-list nil nil 'day)))Misc
let-plist
I don’t need this right now, but it might come in handy here or elsewhere.
(defmacro osa/let-plist (keys plist &rest body)
"`cl-destructuring-bind' without the boilerplate for plists."
;; See https://emacs.stackexchange.com/q/22542/3871
;; I really don't understand why Emacs doesn't have this already.
;; So many things come close to it: pcase, pcase-let, map-let,
;; cl-destructuring-bind, -let...but none of them let you simply
;; bind all the values of a plist to variables with the same name as
;; their keys. You always have to type the name of the key twice.
;; For example, compare these two forms:
;; (-let (((&keys :from from :to to :date date :subject subject) email))
;; (list from to date subject))
;; (osa/let-plist (:from :to :date :subject) email
;; (list from to date subject))
;; Now, sure, sometimes you need to bind values to differently named
;; variables. But when you don't, I know which one I prefer.
(declare (indent defun))
(setq keys (cl-loop for key in keys
collect (intern (replace-regexp-in-string (rx bol ":") ""
(symbol-name key)))))
`(cl-destructuring-bind
(&key ,@keys &allow-other-keys)
,plist
,@body))Profiling
(defmacro profile-it (times &rest body)
`(let (output)
(dolist (p '("org-super-agenda-" "map" "org-" "string-" "s-" "buffer-" "append" "delq" "map" "list" "car" "save-" "outline-" "delete-dups" "sort" "line-" "nth" "concat" "char-to-string" "rx-" "goto-" "when" "search-" "re-"))
(elp-instrument-package p))
(dotimes (x ,times)
,@body)
(elp-results)
(elp-restore-all)
(point-min)
(forward-line 20)
(delete-region (point) (point-max))
(setq output (buffer-substring-no-properties (point-min) (point-max)))
(kill-buffer)
(delete-window)
output))