From c2c7efed00b0f52f80538adee2d7eb7a53ea27c5 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Wed, 11 Nov 2020 20:38:05 -0600 Subject: [PATCH 001/372] Docs: Update readme Links can search more than just the current document now. --- README.org | 2 +- org-ql.info | 42 +++++++++++++++++++++--------------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/README.org b/README.org index 74d8c01..9d68254 100644 --- a/README.org +++ b/README.org @@ -460,7 +460,7 @@ Simple links may also be written manually in either sexp or non-sexp form, like: + Predicate =link=, which matches descriptions and targets in Org links. + Emacs bookmark support: Org QL View buffers can be bookmarked with, e.g. =C-x r m= and shown with, e.g. =C-x r b=. (This also enables view restoration with [[https://github.com/alphapapa/burly.el][Burly]].) + Dynamic block support. -+ Org link support (storing and opening links to Org QL View searches in the current document). ++ Org link support (storing and opening links to Org QL View searches). + Mascot. *Changed* diff --git a/org-ql.info b/org-ql.info index d215a50..2b5c83f 100644 --- a/org-ql.info +++ b/org-ql.info @@ -836,8 +836,8 @@ File: README.info, Node: 05-pre, Next: 046, Up: Changelog view restoration with Burly (https://github.com/alphapapa/burly.el).) • Dynamic block support. - • Org link support (storing and opening links to Org QL View searches - in the current document). + • Org link support (storing and opening links to Org QL View + searches). • Mascot. *Changed* @@ -1288,25 +1288,25 @@ Node: Links28046 Node: Tips28733 Node: Changelog29051 Node: 05-pre29690 -Node: 04630806 -Node: 04531209 -Node: 04431568 -Node: 04331925 -Node: 04232120 -Node: 04132279 -Node: 0432518 -Node: 03236449 -Node: 03136826 -Node: 0337023 -Node: 02339998 -Node: 02240226 -Node: 02140494 -Node: 0240693 -Node: 0144728 -Node: Notes44829 -Node: Comparison with Org Agenda searches44991 -Node: org-sidebar45863 -Node: License46142 +Node: 04630782 +Node: 04531185 +Node: 04431544 +Node: 04331901 +Node: 04232096 +Node: 04132255 +Node: 0432494 +Node: 03236425 +Node: 03136802 +Node: 0336999 +Node: 02339974 +Node: 02240202 +Node: 02140470 +Node: 0240669 +Node: 0144704 +Node: Notes44805 +Node: Comparison with Org Agenda searches44967 +Node: org-sidebar45839 +Node: License46118  End Tag Table From 6ab74454d0493105a1c57889c85800f4df962ebd Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Wed, 11 Nov 2020 20:45:10 -0600 Subject: [PATCH 002/372] Fix: (org-ql-view--expand-buffers-files) Remove eval support This prevents arbitrary code execution when opening "org-ql-search:" links. (The links feature was just pushed to master in the last day or so, and no stable release with the feature has been tagged.) This fix also means that arbitrary expressions are no longer accepted when interactively completing the buffers-files argument to org-ql-search (a worthy trade, I think; users who need to do that can call the function from Lisp). --- README.org | 1 - org-ql-search.el | 1 - org-ql-view.el | 1 - org-ql.info | 75 ++++++++++++++++++++++++------------------------ 4 files changed, 37 insertions(+), 41 deletions(-) diff --git a/README.org b/README.org index 9d68254..7863b92 100644 --- a/README.org +++ b/README.org @@ -106,7 +106,6 @@ Read ~QUERY~ and search with ~org-ql~. Interactively, prompt for these variable + ~buffer~: search the current buffer + ~all~: search all Org buffers + ~agenda~: search buffers returned by the function ~org-agenda-files~ -+ An expression which evaluates to a list of files/buffers + A space-separated list of file or buffer names ~GROUPS~: An ~org-super-agenda~ group set. See variable ~org-super-agenda-groups~. diff --git a/org-ql-search.el b/org-ql-search.el index 0bd02f2..3abc468 100644 --- a/org-ql-search.el +++ b/org-ql-search.el @@ -119,7 +119,6 @@ Interactively, may also be: - `all': search all Org buffers - `agenda': search buffers returned by the function `org-agenda-files' - `directory': search Org files in `org-directory' -- An expression which evaluates to a list of files/buffers - A space-separated list of file or buffer names QUERY: An `org-ql' query in either sexp or non-sexp form (see diff --git a/org-ql-view.el b/org-ql-view.el index bfe72ba..d7a34a8 100644 --- a/org-ql-view.el +++ b/org-ql-view.el @@ -1063,7 +1063,6 @@ The counterpart to `org-ql-view--contract-buffers-files'." ("all" (--select (equal (buffer-local-value 'major-mode it) 'org-mode) (buffer-list))) ("org-directory" (org-ql-search-directories-files)) - ((and form (guard (rx bos "("))) (-flatten (eval (read form)))) (else (s-split (rx (1+ space)) else)))) (defun org-ql-view--complete-super-groups () diff --git a/org-ql.info b/org-ql.info index 2b5c83f..baf149e 100644 --- a/org-ql.info +++ b/org-ql.info @@ -214,7 +214,6 @@ Interactively, may also be: • ‘all’: search all Org buffers • ‘agenda’: search buffers returned by the function ‘org-agenda-files’ - • An expression which evaluates to a list of files/buffers • A space-separated list of file or buffer names ‘GROUPS’: An ‘org-super-agenda’ group set. See variable @@ -1270,43 +1269,43 @@ Node: Quelpa2428 Node: Usage2871 Node: Commands3269 Node: org-ql-search3742 -Node: helm-org-ql5456 -Node: org-ql-view5868 -Node: org-ql-view-sidebar6368 -Node: org-ql-view-recent-items6724 -Node: org-ql-sparse-tree7208 -Node: Queries8008 -Node: Non-sexp query syntax8916 -Node: General predicates10623 -Node: Ancestor/descendant predicates15838 -Node: Date/time predicates16966 -Node: Functions / Macros19621 -Node: Agenda-like views19876 -Node: Listing / acting-on results21281 -Node: Dynamic block25883 -Node: Links28046 -Node: Tips28733 -Node: Changelog29051 -Node: 05-pre29690 -Node: 04630782 -Node: 04531185 -Node: 04431544 -Node: 04331901 -Node: 04232096 -Node: 04132255 -Node: 0432494 -Node: 03236425 -Node: 03136802 -Node: 0336999 -Node: 02339974 -Node: 02240202 -Node: 02140470 -Node: 0240669 -Node: 0144704 -Node: Notes44805 -Node: Comparison with Org Agenda searches44967 -Node: org-sidebar45839 -Node: License46118 +Node: helm-org-ql5392 +Node: org-ql-view5804 +Node: org-ql-view-sidebar6304 +Node: org-ql-view-recent-items6660 +Node: org-ql-sparse-tree7144 +Node: Queries7944 +Node: Non-sexp query syntax8852 +Node: General predicates10559 +Node: Ancestor/descendant predicates15774 +Node: Date/time predicates16902 +Node: Functions / Macros19557 +Node: Agenda-like views19812 +Node: Listing / acting-on results21217 +Node: Dynamic block25819 +Node: Links27982 +Node: Tips28669 +Node: Changelog28987 +Node: 05-pre29626 +Node: 04630718 +Node: 04531121 +Node: 04431480 +Node: 04331837 +Node: 04232032 +Node: 04132191 +Node: 0432430 +Node: 03236361 +Node: 03136738 +Node: 0336935 +Node: 02339910 +Node: 02240138 +Node: 02140406 +Node: 0240605 +Node: 0144640 +Node: Notes44741 +Node: Comparison with Org Agenda searches44903 +Node: org-sidebar45775 +Node: License46054  End Tag Table From 689cfd68e78b69ff19666706b876db97b152bdb1 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Wed, 11 Nov 2020 22:52:54 -0600 Subject: [PATCH 003/372] Fix: (-view--link-open) Set org-super-agenda-allow-unsafe-groups --- org-ql-view.el | 1 + 1 file changed, 1 insertion(+) diff --git a/org-ql-view.el b/org-ql-view.el index d7a34a8..0255b76 100644 --- a/org-ql-view.el +++ b/org-ql-view.el @@ -617,6 +617,7 @@ protocol. See, e.g. `org-ql-view--link-store'." ;; `url-parse-query-string' returns "improper" alists, which makes this awkward. (sort (--when-let (alist-get "sort" params nil nil #'string=) (read it))) + (org-super-agenda-allow-unsafe-groups nil) ; Disallow unsafe group selectors. (groups (--when-let (alist-get "super-groups" params nil nil #'string=) (read it))) (title (--when-let (alist-get "title" params nil nil #'string=) From a5d48868808a8ba59db360466f1c1b1060db16c5 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Thu, 12 Nov 2020 00:22:23 -0600 Subject: [PATCH 004/372] Fix: (org-ql-view--link-open) Safety check the sort parameter --- org-ql-view.el | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/org-ql-view.el b/org-ql-view.el index 0255b76..6945c45 100644 --- a/org-ql-view.el +++ b/org-ql-view.el @@ -615,8 +615,13 @@ protocol. See, e.g. `org-ql-view--link-store'." (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-let (alist-get "sort" params nil nil #'string=) - (read it))) + (sort (when-let* ((stored-string (alist-get "sort" params nil nil #'string=)) + (read-value (read stored-string))) + ;; Ensure the value is either a symbol or list of symbols (which excludes lambdas). + (unless (or (symbolp read-value) (cl-every #'symbolp read-value)) + (error "Potentially unsafe value found in link's SORT parameter (%s). Link not opened" + read-value)) + read-value)) (org-super-agenda-allow-unsafe-groups nil) ; Disallow unsafe group selectors. (groups (--when-let (alist-get "super-groups" params nil nil #'string=) (read it))) From 5e790c48acb91b5969f0147f591fdcd7dfecc7bd Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Thu, 12 Nov 2020 00:22:45 -0600 Subject: [PATCH 005/372] Tests: Add tests for link safety --- tests/test-org-ql.el | 91 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/tests/test-org-ql.el b/tests/test-org-ql.el index 51a2e3d..63a02cf 100644 --- a/tests/test-org-ql.el +++ b/tests/test-org-ql.el @@ -1127,7 +1127,96 @@ RESULTS should be a list of strings as returned by (org-ql-it "Tags and to-do" (org-ql-expect ((and (todo "SOMEDAY") (tags "Emacs"))) - '("Rewrite Emacs in Common Lisp")))))) + '("Rewrite Emacs in Common Lisp"))))) + + (describe "Org link safety" + + ;; NOTE: These tests probably do not guarantee safety. Still, these tests are probably + ;; better than nothing. In fact, this is pretty cool: by testing for the specific error + ;; signal and the arguments to the error, I caught a mistake I made while writing these tests + ;; (leaving a closing bracket off one of the links), which caused that test to fail but for a + ;; different reason. Were it not for testing the specific error AND its arguments, that test + ;; case would have apparently passed, but it would have hidden the mistake in the test. + + ;; Also, while writing these tests, the version of org-super-agenda in the test sandbox does + ;; not have the fix applied yet, and the test for that apparently, correctly does not pass yet. + + (require 'org-ql-search) + (require 'org-ql-view) + + (cl-flet ((open-link + (link) (with-temp-buffer + (org-mode) + (insert link) + (backward-char 1) + (call-interactively #'org-open-at-point)))) + + (it "buffers-files parameter" + (let ((quoted-lambda-link "[[org-ql-search:todo:?buffers-files%3D%28lambda%20nil%20%28message%20%22AHA%22%29%29]]") + (unquoted-lambda-link "[[org-ql-search:todo:?buffers-files%3D%28lambda%20nil%20%28message%20%22AHA%22%29%29]]") + (quoted-lambda-in-list-link "[[org-ql-search:todo:?buffers-files%3D%28%28quote%20%28lambda%20nil%20%28message%20%22AHA%22%29%29%29%29]]") + (unquoted-lambda-in-list-link "[[org-ql-search:todo:?buffers-files%3D%28%28lambda%20nil%20%28message%20%22AHA%22%29%29%29]]")) + (expect (open-link quoted-lambda-link) + :to-throw 'wrong-type-argument '(stringp (lambda nil (message "AHA")))) + (expect (open-link unquoted-lambda-link) + :to-throw 'wrong-type-argument '(stringp (lambda nil (message "AHA")))) + (expect (open-link quoted-lambda-in-list-link) + :to-throw 'wrong-type-argument '(stringp ((quote (lambda nil (message "AHA")))))) + (expect (open-link unquoted-lambda-in-list-link) + :to-throw 'wrong-type-argument '(stringp ((lambda nil (message "AHA"))))))) + + (it "super-groups parameter" + (let ((quoted-lambda-link "[[org-ql-search:todo:?super-groups%3D%28lambda%20nil%20%28message%20%22AHA%22%29%29]]") + (unquoted-lambda-link "[[org-ql-search:todo:?super-groups%3D%28lambda%20nil%20%28message%20%22AHA%22%29%29]]") + (quoted-expression-link "[[org-ql-search:todo:?super-groups%3D%28message%20%22AHA%22%29]]") + (unquoted-expression-link "[[org-ql-search:todo:?super-groups%3D%22AHA%22]]") + (pred-selector-link "[[org-ql-search:todo:?super-groups%3D%28%28%3Apred%20%28lambda%20%28_%29%20%28message%20%22AHA%22%29%29%29%29]]") + (auto-map-selector-link "[[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]]")) + (expect (open-link quoted-lambda-link) + :to-throw 'wrong-type-argument '(listp lambda)) + (expect (open-link unquoted-lambda-link) + :to-throw 'wrong-type-argument '(listp lambda)) + (expect (open-link quoted-expression-link) + :to-throw 'wrong-type-argument '(listp message)) + (expect (open-link unquoted-expression-link) + :to-throw 'error '("cl-etypecase failed: AHA, (symbol list)")) + + ;; FIXME: These two tests will not pass (i.e. they will not + ;; signal an error) until the version of org-super-agenda in + ;; the test sandbox is upgraded, which I'll do when MELPA + ;; packages the latest version. On the bright side, I just + ;; confirmed that, without the fix to org-super-agenda, + ;; these are vulnerable, which means that the test catches + ;; this problem and detects the fix. + (expect (open-link pred-selector-link) + :to-throw 'error '("Unsafe groups disallowed (:pred): (lambda (_) (message AHA))")) + (expect (open-link auto-map-selector-link) + :to-throw 'error '("Unsafe groups disallowed (:auto-map): ((lambda (_) (message AHA)))")))) + + (it "title parameter" + (let ((quoted-lambda-link "[[org-ql-search:todo:?title%3D%28lambda%20%28_%20_%29%20%28message%20%22AHA%22%29%29]]") + (unquoted-lambda-link "[[org-ql-search:todo:?title%3D%28lambda%20%28_%20_%29%20%28message%20%22AHA%22%29%29]]") + (expression-link "[[org-ql-search:todo:?title%3D%28message%20%22AHA%22%29]]")) + (expect (open-link quoted-lambda-link) + :to-throw 'wrong-type-argument '(characterp lambda)) + (expect (open-link unquoted-lambda-link) + :to-throw 'wrong-type-argument '(characterp lambda)) + (expect (open-link expression-link) + :to-throw 'wrong-type-argument '(characterp message)))) + + (it "sort parameter" + (let ((quoted-lambda-link "[[org-ql-search:todo:?sort%3D%28lambda%20%28_%20_%29%20%28message%20%22AHA%22%29%29]]") + (unquoted-lambda-link "[[org-ql-search:todo:?sort%3D%28lambda%20%28_%20_%29%20%28message%20%22AHA%22%29%29]]") + (quoted-lambda-in-list-link "[[org-ql-search:todo:?sort%3D%28%28quote%20%28lambda%20%28_%20_%29%20%28message%20%22AHA%22%29%29%29%29]]") + (unquoted-lambda-in-list-link "[[org-ql-search:todo:?sort=((lambda%20nil%20(message%20\"AHA\")))]]")) + (expect (open-link quoted-lambda-link) + :to-throw 'error '("Potentially unsafe value found in link’s SORT parameter ((lambda (_ _) (message AHA))). Link not opened")) + (expect (open-link unquoted-lambda-link) + :to-throw 'error '("Potentially unsafe value found in link’s SORT parameter ((lambda (_ _) (message AHA))). Link not opened")) + (expect (open-link quoted-lambda-in-list-link) + :to-throw 'error '("Potentially unsafe value found in link’s SORT parameter (((quote (lambda (_ _) (message AHA))))). Link not opened")) + (expect (open-link unquoted-lambda-in-list-link) + :to-throw 'error '("Potentially unsafe value found in link’s SORT parameter (((lambda nil (message AHA)))). Link not opened"))))))) ;; Local Variables: ;; truncate-lines: t From a069cf5b2cd1f0ef3430111feeae4e49765bf47a Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Thu, 12 Nov 2020 00:26:53 -0600 Subject: [PATCH 006/372] Notes: Update --- notes.org | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/notes.org b/notes.org index 3e9aea5..46214e1 100644 --- a/notes.org +++ b/notes.org @@ -70,8 +70,6 @@ - [[#org-agenda-skip-function][org-agenda-skip-function]] - [[#test-caching][Test caching]] - [[#update-commentary][Update commentary]] -- [[#convert-simple-sexp-queries-to-non-sexp][Convert simple sexp queries to non-sexp]] -- [[#tools-for-saving-queries-and-accessing-them-34][Tools for saving queries and accessing them {3/4}]] - [[#outline-path-predicate][Outline path predicate]] - [[#recursive-queries][Recursive queries]] - [[#timeline-view][Timeline view]] @@ -80,7 +78,10 @@ - [[#overlay-based-caching-inspired-by-org-num-mode][Overlay-based caching inspired by org-num-mode]] - [[#alternative-parsing-libraries][Alternative parsing libraries]] - [[#fancier-searching-for-inherited-tags][Fancier searching for inherited tags]] +- [[#convert-simple-sexp-queries-to-non-sexp][Convert simple sexp queries to non-sexp]] +- [[#tools-for-saving-queries-and-accessing-them-34][Tools for saving queries and accessing them {3/4}]] - [[#group-tag-support][Group tag support]] +- [[#checking-links-for-unsafe-parameters][Checking links for unsafe parameters]] - [[#dynamic-blocks][Dynamic blocks]] - [[#helm-command][Helm command]] - [[#quickly-change-sortinggrouping-in-search-views][Quickly change sorting/grouping in search views]] @@ -1167,6 +1168,83 @@ Next steps: + [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 =result= variable +** PROJECT [#A] Checking links for unsafe parameters +:PROPERTIES: +:ID: ba70e375-eddb-40df-8892-fb418c1f70d1 +:END: +:LOGBOOK: +- State "PROJECT" from "UNDERWAY" [2020-11-12 Thu 00:26] +- State "UNDERWAY" from [2020-11-11 Wed 23:09] +:END: + +*** TODO 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. + +*** UNDERWAY Add automated tests +:LOGBOOK: +- 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] +:END: + +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. + +*** DONE Enumerate and test parameters and potentially unsafe types +CLOSED: [2020-11-11 Wed 23:26] +:LOGBOOK: +- State "DONE" from "UNDERWAY" [2020-11-11 Wed 23:26] +- State "UNDERWAY" from [2020-11-11 Wed 23:15] +:END: + +#+CAPTION: Template for making testable links +#+BEGIN_SRC org +[[org-ql-search:todo:?]] +#+END_SRC + +#+CAPTION: Expression to insert encoded values into template (after the =?=) +#+BEGIN_SRC elisp + (insert (url-hexify-string (concat "buffers-files=" (prin1-to-string '((lambda () (message "AHA"))))))) +#+END_SRC + ++ [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] ~:pred~ selector (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-map= selector (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: + +1. 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: + + #+BEGIN_SRC elisp + (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")) + #+END_SRC + +2. Sexp queries already prompt for confirmation, unless the user has set =org-ql-view-ask-unsafe-links= to nil. + +[2020-11-11 Wed 23:27] That's all the parameters and all the types that I can think to test. + ** DONE [#A] Dynamic blocks + *Tasks* From 284eda87e573ba66537797536b9f749fd9804a73 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Thu, 12 Nov 2020 02:10:57 -0600 Subject: [PATCH 007/372] Fix: (org-ql-view--link-store) Test Org version See #147. Thanks to @tpeacock19 for reporting. --- org-ql-view.el | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/org-ql-view.el b/org-ql-view.el index 6945c45..42a170e 100644 --- a/org-ql-view.el +++ b/org-ql-view.el @@ -648,6 +648,8 @@ protocol. See, e.g. `org-ql-view--link-store'." :title title))) (defvar org-ql-view--link-store-counter 0 + ;; TODO: When compatibility with Org <=9.1.4 is dropped, remove this. See + ;; . "Workaround for an idiosyncrasy of `org-store-link' that calls link-storing functions twice.") (defun org-ql-view--link-store () @@ -661,11 +663,13 @@ When opened, the link searches the buffer it's opened from." (cl-some #'bufferp org-ql-view-buffers-files)) ;; Buffers are unreadable, so they can't be linked to. (user-error "Views that search buffers rather than files can't be linked to")) - (cl-incf org-ql-view--link-store-counter) + (cl-incf org-ql-view--link-store-counter) ;; TODO: Remove when not supporting Org<=9.1.4. See other comment. (cl-flet ((prompt-for (buffers-files) ;; HACK: Use counter to avoid prompting the first of the ;; two times that `org-store-link' calls this function. - (when (cl-evenp org-ql-view--link-store-counter) + ;; TODO: Remove the version check when not supporting Org<=9.1.4. See other comment. + (when (or (version< "9.1.4" (org-version)) + (cl-evenp org-ql-view--link-store-counter)) (pcase-exhaustive (completing-read (format "Link to search file containing inserted link or %s? " buffers-files) (list "containing file" buffers-files) nil t) From f63012dd5adb9d3842a7790c5cbc45edf88fbcb2 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Thu, 12 Nov 2020 02:43:08 -0600 Subject: [PATCH 008/372] Fix: (org-ql-view--link-store) Rejection of unlinkable buffers Also, remove the Org <=9.1.4 compatibility code. It's not worth it. I'm probably the only person who needs it, anyway, and I can live with the double-call until I upgrade my Org version. Fixes #148. Thanks to @tpeacock19 for reporting. --- org-ql-view.el | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/org-ql-view.el b/org-ql-view.el index 42a170e..843211e 100644 --- a/org-ql-view.el +++ b/org-ql-view.el @@ -647,11 +647,6 @@ protocol. See, e.g. `org-ql-view--link-store'." :super-groups groups :title title))) -(defvar org-ql-view--link-store-counter 0 - ;; TODO: When compatibility with Org <=9.1.4 is dropped, remove this. See - ;; . - "Workaround for an idiosyncrasy of `org-store-link' that calls link-storing functions twice.") - (defun org-ql-view--link-store () "Store a link to the current Org QL view. When opened, the link searches the buffer it's opened from." @@ -659,22 +654,20 @@ When opened, the link searches the buffer it's opened from." (require 'url-util) (when org-ql-view-query ;; Only Org QL View buffers should have `org-ql-view-query' set. - (when (or (bufferp org-ql-view-buffers-files) - (cl-some #'bufferp org-ql-view-buffers-files)) - ;; Buffers are unreadable, so they can't be linked to. - (user-error "Views that search buffers rather than files can't be linked to")) - (cl-incf org-ql-view--link-store-counter) ;; TODO: Remove when not supporting Org<=9.1.4. See other comment. (cl-flet ((prompt-for (buffers-files) - ;; HACK: Use counter to avoid prompting the first of the - ;; two times that `org-store-link' calls this function. - ;; TODO: Remove the version check when not supporting Org<=9.1.4. See other comment. - (when (or (version< "9.1.4" (org-version)) - (cl-evenp org-ql-view--link-store-counter)) - (pcase-exhaustive - (completing-read (format "Link to search file containing inserted link or %s? " buffers-files) - (list "containing file" buffers-files) nil t) - ("containing file" nil) - (buffers-files (prin1-to-string buffers-files)))))) + (pcase-exhaustive + (completing-read (format "Link to search file containing inserted link or %s? " buffers-files) + (list "containing file" buffers-files) nil t) + ("containing file" nil) + (buffers-files (prin1-to-string buffers-files)))) + (string-or-file-buffer-p + (thing) (or (stringp thing) + (and (bufferp thing) + (buffer-file-name thing))))) + (unless (or (string-or-file-buffer-p org-ql-view-buffers-files) + (and (listp org-ql-view-buffers-files) + (cl-every #'string-or-file-buffer-p org-ql-view-buffers-files))) + (user-error "Views that search non-file-backed buffers can't be linked to")) (let* ((query-string (--if-let (org-ql--query-sexp-to-string org-ql-view-query) it (prin1-to-string (org-ql-view--format-query org-ql-view-query)))) (buffers-files (prompt-for (org-ql-view--contract-buffers-files org-ql-view-buffers-files))) From cbd4ea89f4f044719a01cf189a2a85efeaaec889 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Thu, 12 Nov 2020 03:18:16 -0600 Subject: [PATCH 009/372] Notes: Update --- notes.org | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/notes.org b/notes.org index 46214e1..451e6fb 100644 --- a/notes.org +++ b/notes.org @@ -18,19 +18,18 @@ ** Underway -[[org-ql-search:%2528todo%2520%2522UNDERWAY%2522%2529?sort=%2528priority%2529&title=%2522Underway%2522][org-ql-search: Underway]] +[[org-ql-search:todo%253AUNDERWAY?sort=%2528priority%2529&title=%2522Underway%2522][org-ql-search: Underway]] #+BEGIN: org-ql :query (todo "UNDERWAY") :columns ((priority "P") (todo "Keyword") heading) :sort (priority date) :ts-format "%Y-%m-%d %H:%M" | P | Keyword | Heading | |---+----------+--------------------------------------------------------------------| -| A | UNDERWAY | [[Tools%20for%20saving%20queries%20and%20accessing%20them%20%5B3/4%5D][Tools for saving queries and accessing them {3/4}]] | -| A | UNDERWAY | [[Org%20link%20types%20%5B2/3%5D][Org link types {2/3}]] | | A | UNDERWAY | [[Outline path predicate][Outline path predicate]] | | B | UNDERWAY | [[Recursive queries][Recursive queries]] | | B | UNDERWAY | [[Timeline view][Timeline view]] | | B | UNDERWAY | [["Node" caching]["Node" caching]] | | B | UNDERWAY | [[Implement view with tabulated-list-mode or magit-section][Implement view with tabulated-list-mode or magit-section]] | | | UNDERWAY | [[Benchmarking tags searches without and with new group-tags support][Benchmarking tags searches without and with new group-tags support]] | +| | UNDERWAY | [[Add automated tests][Add automated tests]] | #+END: ** To-do @@ -46,7 +45,7 @@ | A | TODO | [[Change ~(deadline)~'s ~auto~ argument to ~:auto~ and/or ~:auto t~][Change ~(deadline)~'s ~auto~ argument to ~:auto~ and/or ~:auto t~]] | | A | TODO | [[Save views to dynamic blocks][Save views to dynamic blocks]] | | A | TODO | [[Consider how to "secure" searches from links and dynamic blocks][Consider how to "secure" searches from links and dynamic blocks]] | -| A | UNDERWAY | [[Tools%20for%20saving%20queries%20and%20accessing%20them%20%5B3/4%5D][Tools for saving queries and accessing them {3/4}]] | +| A | UNDERWAY | [[Outline path predicate][Outline path predicate]] | #+END: * [#A] Tasks @@ -64,6 +63,7 @@ - [[#add-more-sorters][Add more sorters?]] - [[#default-sort][Default sort]] - [[#partial-match-for-property-queries][Partial match for property queries]] +- [[#move-this-notes-file-into-an-orphan-metanotes-branch][Move this notes file into an orphan meta/notes branch]] - [[#new-transient-transient-lisp-variable-class][New Transient transient-lisp-variable class]] - [[#normalize-queries][Normalize queries]] - [[#update-view-screenshots][Update view screenshots]] @@ -162,6 +162,10 @@ e.g. something like [[https://200ok.ch/posts/2020-02-09_creating_org_mode_sparse 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. +** TODO [#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. + ** TODO [#C] [[https://github.com/magit/transient/issues/76][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. @@ -1181,7 +1185,7 @@ Next steps: That /should/ force the version of org-super-agenda with the fix to be installed when org-ql is upgraded. -*** UNDERWAY Add automated tests +*** DONE Add automated tests :LOGBOOK: - State "UNDERWAY" from "TODO" [2020-11-12 Thu 00:24] - State "TODO" from "MAYBE" [2020-11-11 Wed 23:16] @@ -1245,7 +1249,13 @@ For the query expression: [2020-11-11 Wed 23:27] That's all the parameters and all the types that I can think to test. -** DONE [#A] Dynamic blocks +** PROJECT Dynamic blocks + +*** TODO [#B] Save view to dynamic block + +[2020-11-12 Thu 03:23] A command to do this would be very helpful. + +*** DONE [#A] Implement dynamic blocks + *Tasks* - [X] Merge code From 337bfa6695bb5c3df83904311c0350499a2f3751 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Thu, 12 Nov 2020 04:09:57 -0600 Subject: [PATCH 010/372] Add: (tags-regexp, tags*) Predicate --- README.org | 3 +++ org-ql.el | 20 ++++++++++++++ org-ql.info | 64 ++++++++++++++++++++++++-------------------- tests/test-org-ql.el | 28 +++++++++++++++++++ 4 files changed, 86 insertions(+), 29 deletions(-) diff --git a/README.org b/README.org index 7863b92..6fcfc92 100644 --- a/README.org +++ b/README.org @@ -219,6 +219,8 @@ Arguments are listed next to predicate names, where applicable. - Aliases: ~local-tags~, ~tags-l~, ~ltags~. + =tags-all (tags)= :: Return non-nil if current heading includes all of ~TAGS~. Tests both inherited and local tags. - Aliases: ~tags&~. ++ =tags-regexp (&rest regexps)= :: Return non-nil if current heading has tags matching one or more of ~REGEXPS~. Tests both inherited and local tags. + - Aliases: ~tags*~. + =todo (&optional keywords)= :: Return non-nil if current heading is a ~TODO~ item. With ~KEYWORDS~, return non-nil if its keyword is one of ~KEYWORDS~ (a list of strings). When called without arguments, only matches non-done tasks (i.e. does not match keywords in ~org-done-keywords~). *** Ancestor/descendant predicates @@ -457,6 +459,7 @@ Simple links may also be written manually in either sexp or non-sexp form, like: *Added* + View dispatcher using =transient.el= (like Magit), bound to =v= in search/view buffers. + Predicate =link=, which matches descriptions and targets in Org links. ++ Predicate ~tags-regexp~ (alias: ~tags*~), which matches regexps against entry tags (e.g, helpful when a tag might end in "s"). + Emacs bookmark support: Org QL View buffers can be bookmarked with, e.g. =C-x r m= and shown with, e.g. =C-x r b=. (This also enables view restoration with [[https://github.com/alphapapa/burly.el][Burly]].) + Dynamic block support. + Org link support (storing and opening links to Org QL View searches). diff --git a/org-ql.el b/org-ql.el index f6639c4..dc6ee24 100644 --- a/org-ql.el +++ b/org-ql.el @@ -672,6 +672,7 @@ Replaces bare strings with (regexp) selectors, and appropriate ;; Inherited and local predicate aliases. (`(,(or 'tags-i 'itags 'inherited-tags) . ,tags) `(tags-inherited ,@tags)) (`(,(or 'tags-l 'ltags 'local-tags) . ,tags) `(tags-local ,@tags)) + (`(,(or 'tags*) . ,regexps) `(tags-regexp ,@regexps)) ;; Timestamps (`(,(or 'ts-active 'ts-a) . ,rest) `(ts :type active ,@rest)) @@ -1153,6 +1154,25 @@ If TAGS is nil, return non-nil if heading has any local tags." (otherwise (when (tags-p local) (seq-intersection tags local))))))) +(org-ql--defpred (tags-regexp tags*) (&rest regexps) + "Return non-nil if current heading has tags matching one or more of REGEXPS. +Tests both inherited and local tags." + (cl-macrolet ((tags-p (tags) + `(and ,tags + (not (eq 'org-ql-nil ,tags))))) + (-let* (((inherited local) (org-ql--tags-at (point)))) + (cl-typecase regexps + (null (or (tags-p inherited) + (tags-p local))) + (otherwise (or (when (tags-p inherited) + (cl-loop for tag in inherited + thereis (cl-loop for regexp in regexps + thereis (string-match regexp tag)))) + (when (tags-p local) + (cl-loop for tag in local + thereis (cl-loop for regexp in regexps + thereis (string-match regexp tag)))))))))) + (org-ql--defpred level (level-or-comparator &optional level) "Return non-nil if current heading's outline level matches arguments. The following forms are accepted: diff --git a/org-ql.info b/org-ql.info index baf149e..3d86d66 100644 --- a/org-ql.info +++ b/org-ql.info @@ -455,6 +455,10 @@ Arguments are listed next to predicate names, where applicable. Return non-nil if current heading includes all of ‘TAGS’. Tests both inherited and local tags. • Aliases: ‘tags&’. +‘tags-regexp (&rest regexps)’ + Return non-nil if current heading has tags matching one or more of + ‘REGEXPS’. Tests both inherited and local tags. + • Aliases: ‘tags*’. ‘todo (&optional keywords)’ Return non-nil if current heading is a ‘TODO’ item. With ‘KEYWORDS’, return non-nil if its keyword is one of ‘KEYWORDS’ (a @@ -830,6 +834,8 @@ File: README.info, Node: 05-pre, Next: 046, Up: Changelog search/view buffers. • Predicate link, which matches descriptions and targets in Org links. + • Predicate ‘tags-regexp’ (alias: ‘tags*’), which matches regexps + against entry tags (e.g, helpful when a tag might end in "s"). • Emacs bookmark support: Org QL View buffers can be bookmarked with, e.g. C-x r m and shown with, e.g. C-x r b. (This also enables view restoration with Burly @@ -1277,35 +1283,35 @@ Node: org-ql-sparse-tree7144 Node: Queries7944 Node: Non-sexp query syntax8852 Node: General predicates10559 -Node: Ancestor/descendant predicates15774 -Node: Date/time predicates16902 -Node: Functions / Macros19557 -Node: Agenda-like views19812 -Node: Listing / acting-on results21217 -Node: Dynamic block25819 -Node: Links27982 -Node: Tips28669 -Node: Changelog28987 -Node: 05-pre29626 -Node: 04630718 -Node: 04531121 -Node: 04431480 -Node: 04331837 -Node: 04232032 -Node: 04132191 -Node: 0432430 -Node: 03236361 -Node: 03136738 -Node: 0336935 -Node: 02339910 -Node: 02240138 -Node: 02140406 -Node: 0240605 -Node: 0144640 -Node: Notes44741 -Node: Comparison with Org Agenda searches44903 -Node: org-sidebar45775 -Node: License46054 +Node: Ancestor/descendant predicates15972 +Node: Date/time predicates17100 +Node: Functions / Macros19755 +Node: Agenda-like views20010 +Node: Listing / acting-on results21415 +Node: Dynamic block26017 +Node: Links28180 +Node: Tips28867 +Node: Changelog29185 +Node: 05-pre29824 +Node: 04631063 +Node: 04531466 +Node: 04431825 +Node: 04332182 +Node: 04232377 +Node: 04132536 +Node: 0432775 +Node: 03236706 +Node: 03137083 +Node: 0337280 +Node: 02340255 +Node: 02240483 +Node: 02140751 +Node: 0240950 +Node: 0144985 +Node: Notes45086 +Node: Comparison with Org Agenda searches45248 +Node: org-sidebar46120 +Node: License46399  End Tag Table diff --git a/tests/test-org-ql.el b/tests/test-org-ql.el index 63a02cf..d31a08f 100644 --- a/tests/test-org-ql.el +++ b/tests/test-org-ql.el @@ -962,6 +962,34 @@ RESULTS should be a list of strings as returned by '("Fruit" "Blueberry" "Strawberry") :buffer (org-ql-test-data-buffer "data2.org")))) + (describe "(tags-regexp), (tags*)" + + (org-ql-it "without arguments" + (org-ql-expect ((tags-regexp)) + '("Take over the universe" "Take over the world" "Skype with president of Antarctica" "Take over Mars" "Visit Mars" "Take over the moon" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp" "Write a symphony")) + (org-ql-expect ((not (tags*))) + '("Recurring" "Sunrise/sunset" "Ideas"))) + + (org-ql-it "with a tag regexp" + (org-ql-expect ((tags-regexp "Emac")) + '("/r/emacs" "Rewrite Emacs in Common Lisp")) + (org-ql-expect ((not (tags* "Emac"))) + '("Take over the universe" "Take over the world" "Skype with president of Antarctica" "Take over Mars" "Visit Mars" "Take over the moon" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "Recurring" "Shop for groceries" "Sunrise/sunset" "Ideas" "Write a symphony"))) + + (org-ql-it "with 2 tag regexps" + (org-ql-expect ((tags-regexp "Emac" "spac")) + '("Visit Mars" "Visit the moon" "Spaceship lease" "Fix flux capacitor" "/r/emacs" "Rewrite Emacs in Common Lisp")) + (org-ql-expect ((not (tags* "Emac" "spac"))) + '("Take over the universe" "Take over the world" "Skype with president of Antarctica" "Take over Mars" "Take over the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "Order a pizza" "Get haircut" "Internet" "Recurring" "Shop for groceries" "Sunrise/sunset" "Ideas" "Write a symphony"))) + + (org-ql-it "with regexp matching file tags" + (org-ql-expect ((tags-regexp "foo")) + '("Fruit" "Blueberry" "Strawberry" "Vegetable" "Broccoli" "Potato") + :buffer (org-ql-test-data-buffer "data2.org")) + (org-ql-expect ((tags* "frui")) + '("Fruit" "Blueberry" "Strawberry") + :buffer (org-ql-test-data-buffer "data2.org")))) + (describe "(ts)" (describe "active" From 2b21871a02f865fc5c16a030442af3fd58bb9c60 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Thu, 12 Nov 2020 04:13:47 -0600 Subject: [PATCH 011/372] Notes: Update --- notes.org | 540 +++++++++++++++++++++++++++--------------------------- 1 file changed, 270 insertions(+), 270 deletions(-) diff --git a/notes.org b/notes.org index 451e6fb..bbfa442 100644 --- a/notes.org +++ b/notes.org @@ -267,233 +267,6 @@ To implement this with good performance probably needs an outline-path cache. I [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. -** UNDERWAY [#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). - -#+BEGIN_SRC elisp - (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)))) -#+END_SRC - -** UNDERWAY [#B] Timeline view -:PROPERTIES: -:ID: 00573552-ffe9-4608-8904-7f6c73246b6d -:END: - -e.g. as mentioned by Samuel Wales at https://lists.gnu.org/archive/html/emacs-orgmode/2019-08/msg00330.html. Prototype code: - -#+BEGIN_SRC elisp - (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))) -#+END_SRC - -Another, more up-to-date implementation: - -#+BEGIN_SRC elisp - ;; 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") -#+END_SRC - -[2019-09-26 Thu 21:28] Would probably make sense to implement this using the view-sections someday. - ** UNDERWAY [#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. @@ -598,49 +371,6 @@ This works okay (except the priority accessor needs to be fixed, because Org pri :action #'helm-org-goto-marker)))) #+END_SRC -** UNDERWAY [#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: - -#+BEGIN_SRC elisp - (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)) -#+END_SRC - -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 - -**** [[https://github.com/m2ym/direx-el][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 - ** 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. @@ -1249,6 +979,276 @@ For the query expression: [2020-11-11 Wed 23:27] That's all the parameters and all the types that I can think to test. +** 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). + +#+BEGIN_SRC elisp + (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)))) +#+END_SRC + +** PROJECT [#B] Timeline view +:PROPERTIES: +:ID: 00573552-ffe9-4608-8904-7f6c73246b6d +:END: + +e.g. as mentioned by Samuel Wales at https://lists.gnu.org/archive/html/emacs-orgmode/2019-08/msg00330.html. Prototype code: + +#+BEGIN_SRC elisp + (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))) +#+END_SRC + +Another, more up-to-date implementation: + +#+BEGIN_SRC elisp + ;; 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") +#+END_SRC + +[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: + +#+BEGIN_SRC elisp + (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)) +#+END_SRC + +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 + +**** [[https://github.com/m2ym/direx-el][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 *** TODO [#B] Save view to dynamic block From cd95d10f05f1d770c17b6d70534578a11a0357d2 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Thu, 12 Nov 2020 04:55:43 -0600 Subject: [PATCH 012/372] Add: Dynamic block (property) column --- README.org | 27 +++++++++-------- notes.org | 33 ++++++++++++-------- org-ql-search.el | 31 +++++++++++++------ org-ql.info | 78 ++++++++++++++++++++++++++---------------------- 4 files changed, 99 insertions(+), 70 deletions(-) diff --git a/README.org b/README.org index 6fcfc92..8794c33 100644 --- a/README.org +++ b/README.org @@ -404,29 +404,30 @@ Expands into a call to ~org-ql-select~ with the same arguments. For convenience Org QL provides a dynamic block that lists entries in the current document matching a query. In the header, these parameters are supported: + ~:query~: An Org QL query expression in either sexp or non-sexp form. -+ ~:columns~ A list of columns, including ~heading~, ~todo~, ~priority~, ~deadline~, ~scheduled~. - - Each column may also be specified as a list with the second element being a header string. For example, to abbreviate the priority column: ~(priority "P")~ ++ ~:columns~ A list of columns, including ~heading~, ~todo~, ~property~, ~priority~, ~deadline~, ~scheduled~. + - Each column may also be specified as a list with the second element being a header string. For example, to abbreviate the priority column: ~(priority "P")~. + - For certain columns, like =property=, arguments may be passed by specifying the column type itself as a list. For example, to display a column showing the values of a ~property~ named ~milestone~, with the header being abbreviated to ~M~: ~((property "milestone") "M")~. + ~:sort~ One or a list of Org QL sorting methods (see ~org-ql-select~). + ~:take~ Optionally take a number of results from the front (a positive number) or the end (a negative number) of the results. + ~:ts-format~ Optional format string used to format timestamp-based columns. The heading column is formatted as a link to the heading (not shown in the following example). -For example, this dynamic block shows the first seven headings that are to-do items with priority A or B, sorted by deadline then priority, with certain columns and timestamp format: +For example, this dynamic block shows the first seven headings that are to-do items with priority A or B, sorted by deadline then priority, with certain columns (including the value of the =agenda-group= property with a custom header) and timestamp format: # NOTE: These results are edited manually because the Org links don't display well in the Info manual. #+BEGIN_SRC org - ,#+BEGIN: org-ql :query "todo: priority:A,B" :columns (todo (priority "P") deadline heading) :sort (deadline priority) :take 7 :ts-format "%Y-%m-%d %H:%M" - | Todo | P | Deadline | Heading | - |------+---+------------------+---------------------------------------| - | TODO | A | 2017-07-07 00:00 | Take over the world | - | TODO | B | 2017-07-10 00:00 | Renew membership in supervillain club | - | TODO | A | 2017-07-15 00:00 | Take over the universe | - | TODO | B | 2017-07-21 00:00 | Internet | - | TODO | A | 2017-08-01 00:00 | Spaceship lease | - | TODO | A | | Skype with president of Antarctica | - | TODO | B | | Take over Mars | + ,#+BEGIN: org-ql :query "todo: priority:A,B" :columns (todo (priority "P") ((property "agenda-group") "Group") deadline heading) :sort (deadline priority) :take 7 :ts-format "%Y-%m-%d %H:%M" + | Todo | P | Group | Deadline | Heading | + |------+---+-------+------------------+---------------------------------------| + | TODO | A | | 2017-07-07 00:00 | Take over the world | + | TODO | B | | 2017-07-10 00:00 | Renew membership in supervillain club | + | TODO | A | plans | 2017-07-15 00:00 | Take over the universe | + | TODO | B | | 2017-07-21 00:00 | Internet | + | TODO | A | bills | 2017-08-01 00:00 | Spaceship lease | + | TODO | A | | | Skype with president of Antarctica | + | TODO | B | | | Take over Mars | ,#+END: #+END_SRC diff --git a/notes.org b/notes.org index bbfa442..3718c69 100644 --- a/notes.org +++ b/notes.org @@ -24,12 +24,8 @@ | P | Keyword | Heading | |---+----------+--------------------------------------------------------------------| | A | UNDERWAY | [[Outline path predicate][Outline path predicate]] | -| B | UNDERWAY | [[Recursive queries][Recursive queries]] | -| B | UNDERWAY | [[Timeline view][Timeline view]] | | B | UNDERWAY | [["Node" caching]["Node" caching]] | -| B | UNDERWAY | [[Implement view with tabulated-list-mode or magit-section][Implement view with tabulated-list-mode or magit-section]] | | | UNDERWAY | [[Benchmarking tags searches without and with new group-tags support][Benchmarking tags searches without and with new group-tags support]] | -| | UNDERWAY | [[Add automated tests][Add automated tests]] | #+END: ** To-do @@ -48,6 +44,16 @@ | A | UNDERWAY | [[Outline path predicate][Outline path predicate]] | #+END: +** Milestones + +[[org-ql-search:todo%253A?super-groups=%2528%2528%253Aauto-property%2520%2522milestone%2522%2529%2529&sort=%2528priority%2529&title=%2522Milestones%2522][org-ql-search: Milestones]] + +#+BEGIN: org-ql :query "todo: property:milestone" :columns (((property "milestone") "M") (priority "P") todo heading) :sort (priority date) :take 7 +| M | P | Todo | Heading | +|-----+---+------+----------------------------| +| 0.6 | B | TODO | [[Save view to dynamic block][Save view to dynamic block]] | +#+END: + * [#A] Tasks :PROPERTIES: :TOC: :include descendants :depth 1 @@ -68,13 +74,9 @@ - [[#normalize-queries][Normalize queries]] - [[#update-view-screenshots][Update view screenshots]] - [[#org-agenda-skip-function][org-agenda-skip-function]] -- [[#test-caching][Test caching]] - [[#update-commentary][Update commentary]] - [[#outline-path-predicate][Outline path predicate]] -- [[#recursive-queries][Recursive queries]] -- [[#timeline-view][Timeline view]] - [[#node-caching]["Node" caching]] -- [[#implement-view-with-tabulated-list-mode-or-magit-section][Implement view with tabulated-list-mode or magit-section]] - [[#overlay-based-caching-inspired-by-org-num-mode][Overlay-based caching inspired by org-num-mode]] - [[#alternative-parsing-libraries][Alternative parsing libraries]] - [[#fancier-searching-for-inherited-tags][Fancier searching for inherited tags]] @@ -82,7 +84,11 @@ - [[#tools-for-saving-queries-and-accessing-them-34][Tools for saving queries and accessing them {3/4}]] - [[#group-tag-support][Group tag support]] - [[#checking-links-for-unsafe-parameters][Checking links for unsafe parameters]] +- [[#recursive-queries][Recursive queries]] +- [[#timeline-view][Timeline view]] +- [[#implement-view-with-tabulated-list-mode-or-magit-section][Implement view with tabulated-list-mode or magit-section]] - [[#dynamic-blocks][Dynamic blocks]] +- [[#test-caching][Test caching]] - [[#helm-command][Helm command]] - [[#quickly-change-sortinggrouping-in-search-views][Quickly change sorting/grouping in search views]] - [[#byte-compile-lambdas][Byte-compile lambdas]] @@ -226,10 +232,6 @@ As discussed [[https://www.reddit.com/r/emacs/comments/cnrt2d/orgqlblock_integra 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... -** TODO [#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. - ** TODO [#C] Update commentary ** UNDERWAY [#A] Outline path predicate @@ -1252,6 +1254,9 @@ Appears to be another implementation of magit-section-like expandable sections. ** PROJECT Dynamic blocks *** TODO [#B] Save view to dynamic block +:PROPERTIES: +:milestone: 0.6 +:END: [2020-11-12 Thu 03:23] A command to do this would be very helpful. @@ -1334,6 +1339,10 @@ For example, [[https://egli.dev/posts/using-org-mode-for-meeting-minutes/][this [2020-11-10 Tue 00:03] I think it's ready to merge now. +** 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. + ** DONE [#A] Helm command In branch =wip/helm-org-ql=. Works really well, should add it and demonstrate it. diff --git a/org-ql-search.el b/org-ql-search.el index 3abc468..2c4b213 100644 --- a/org-ql-search.el +++ b/org-ql-search.el @@ -247,10 +247,17 @@ Valid parameters include: form. :columns A list of columns, including `heading', `todo', - `priority', `deadline', `scheduled'. Each column may - also be specified as a list with the second element - being a header string. For example, to abbreviate - the priority column: (priority \"P\") + `property', `priority', `deadline', `scheduled'. + Each column may also be specified as a list with the + second element being a header string. For example, + to abbreviate the priority column: (priority \"P\"). + For certain columns, like `property', arguments may + be passed by specifying the column type itself as a + list. For example, to display a column showing the + values of a property named \"milestone\", with the + header being abbreviated to \"M\": + + ((property \"milestone\") \"M\"). :sort One or a list of Org QL sorting methods (see `org-ql-select'). @@ -287,7 +294,9 @@ For example, an org-ql dynamic block header could look like: (ts-format ts-format (ts-parse-org-element it))))) (cons 'scheduled (lambda (element) (--when-let (org-element-property :scheduled element) - (ts-format ts-format (ts-parse-org-element it))))))) + (ts-format ts-format (ts-parse-org-element it))))) + (cons 'property (lambda (element property &optional value) + (org-element-property (intern (concat ":" (upcase property))) element))))) (elements (org-ql-query :from (current-buffer) :where query :select '(org-element-headline-parser (line-end-position)) @@ -298,10 +307,14 @@ For example, an org-ql dynamic block header could look like: (integer (-take take elements))))) (cl-labels ((format-element (element) (string-join (cl-loop for column in columns - for fn = (cl-etypecase column - (symbol (alist-get column format-fns)) - (list (alist-get (car column) format-fns))) - collect (or (funcall fn element) "")) + collect (or (pcase-exhaustive column + ((pred symbolp) + (funcall (alist-get column format-fns) element)) + (`((,column . ,args) ,header) + (apply (alist-get column format-fns) element args)) + (`(,column ,header) + (funcall (alist-get column format-fns) element))) + "")) " | "))) ;; Table header (insert "| " (string-join (--map (pcase it diff --git a/org-ql.info b/org-ql.info index 3d86d66..9739c60 100644 --- a/org-ql.info +++ b/org-ql.info @@ -730,10 +730,15 @@ supported: • ‘:query’: An Org QL query expression in either sexp or non-sexp form. • ‘:columns’ A list of columns, including ‘heading’, ‘todo’, - ‘priority’, ‘deadline’, ‘scheduled’. + ‘property’, ‘priority’, ‘deadline’, ‘scheduled’. • Each column may also be specified as a list with the second element being a header string. For example, to abbreviate the - priority column: ‘(priority "P")’ + priority column: ‘(priority "P")’. + • For certain columns, like property, arguments may be passed by + specifying the column type itself as a list. For example, to + display a column showing the values of a ‘property’ named + ‘milestone’, with the header being abbreviated to ‘M’: + ‘((property "milestone") "M")’. • ‘:sort’ One or a list of Org QL sorting methods (see ‘org-ql-select’). • ‘:take’ Optionally take a number of results from the front (a @@ -746,18 +751,19 @@ in the following example). For example, this dynamic block shows the first seven headings that are to-do items with priority A or B, sorted by deadline then priority, -with certain columns and timestamp format: +with certain columns (including the value of the agenda-group property +with a custom header) and timestamp format: - #+BEGIN: org-ql :query "todo: priority:A,B" :columns (todo (priority "P") deadline heading) :sort (deadline priority) :take 7 :ts-format "%Y-%m-%d %H:%M" - | Todo | P | Deadline | Heading | - |------+---+------------------+---------------------------------------| - | TODO | A | 2017-07-07 00:00 | Take over the world | - | TODO | B | 2017-07-10 00:00 | Renew membership in supervillain club | - | TODO | A | 2017-07-15 00:00 | Take over the universe | - | TODO | B | 2017-07-21 00:00 | Internet | - | TODO | A | 2017-08-01 00:00 | Spaceship lease | - | TODO | A | | Skype with president of Antarctica | - | TODO | B | | Take over Mars | + #+BEGIN: org-ql :query "todo: priority:A,B" :columns (todo (priority "P") ((property "agenda-group") "Group") deadline heading) :sort (deadline priority) :take 7 :ts-format "%Y-%m-%d %H:%M" + | Todo | P | Group | Deadline | Heading | + |------+---+-------+------------------+---------------------------------------| + | TODO | A | | 2017-07-07 00:00 | Take over the world | + | TODO | B | | 2017-07-10 00:00 | Renew membership in supervillain club | + | TODO | A | plans | 2017-07-15 00:00 | Take over the universe | + | TODO | B | | 2017-07-21 00:00 | Internet | + | TODO | A | bills | 2017-08-01 00:00 | Spaceship lease | + | TODO | A | | | Skype with president of Antarctica | + | TODO | B | | | Take over Mars | #+END:  @@ -1289,29 +1295,29 @@ Node: Functions / Macros19755 Node: Agenda-like views20010 Node: Listing / acting-on results21415 Node: Dynamic block26017 -Node: Links28180 -Node: Tips28867 -Node: Changelog29185 -Node: 05-pre29824 -Node: 04631063 -Node: 04531466 -Node: 04431825 -Node: 04332182 -Node: 04232377 -Node: 04132536 -Node: 0432775 -Node: 03236706 -Node: 03137083 -Node: 0337280 -Node: 02340255 -Node: 02240483 -Node: 02140751 -Node: 0240950 -Node: 0144985 -Node: Notes45086 -Node: Comparison with Org Agenda searches45248 -Node: org-sidebar46120 -Node: License46399 +Node: Links28715 +Node: Tips29402 +Node: Changelog29720 +Node: 05-pre30359 +Node: 04631598 +Node: 04532001 +Node: 04432360 +Node: 04332717 +Node: 04232912 +Node: 04133071 +Node: 0433310 +Node: 03237241 +Node: 03137618 +Node: 0337815 +Node: 02340790 +Node: 02241018 +Node: 02141286 +Node: 0241485 +Node: 0145520 +Node: Notes45621 +Node: Comparison with Org Agenda searches45783 +Node: org-sidebar46655 +Node: License46934  End Tag Table From 56647d4b1159f3dc3e638ea2e7b1a85446884ca5 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Thu, 12 Nov 2020 05:23:59 -0600 Subject: [PATCH 013/372] Notes: Update --- notes.org | 99 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 59 insertions(+), 40 deletions(-) diff --git a/notes.org b/notes.org index 3718c69..586045f 100644 --- a/notes.org +++ b/notes.org @@ -30,7 +30,7 @@ ** To-do -[[org-ql-search:todo:?super-groups=%2528%2528%253Aauto-outline-path%2529%2529&sort=%2528priority%2529][org-ql-search: Tasks]] ++ [[org-ql-search:todo:?super-groups=%2528%2528%253Aauto-outline-path%2529%2529&sort=%2528priority%2529][org-ql-search: Tasks]] #+BEGIN: org-ql :query "todo: priority:A" :columns ((priority "P") todo heading) :sort (priority date) :take 7 | P | Todo | Heading | @@ -44,6 +44,20 @@ | A | UNDERWAY | [[Outline path predicate][Outline path predicate]] | #+END: +** [[org-ql-search:(and%20(todo%20"PROJECT")%20(not%20(descendants%20(todo%20"NEXT"))))?super-groups=%2528%2528%253Aauto-outline-path%2529%2529&sort=%2528priority%2529&title=%2522Stuck%2520Projects%2522][Stuck projects]] + +#+BEGIN: org-ql :query (and (todo "PROJECT") (not (descendants (todo "NEXT")))) :columns ((priority "P") heading) :sort (priority date) :take 7 +| P | Heading | +|---+---------------------------------------------------| +| A | [[Outline path predicate][Outline path predicate]] | +| A | [[Convert simple sexp queries to non-sexp][Convert simple sexp queries to non-sexp]] | +| A | [[Tools%20for%20saving%20queries%20and%20accessing%20them%20%5B3/4%5D][Tools for saving queries and accessing them {3/4}]] | +| A | [[Org%20link%20types%20%5B2/3%5D][Org link types {2/3}]] | +| A | [[Group tag support][Group tag support]] | +| A | [[Checking links for unsafe parameters][Checking links for unsafe parameters]] | +| B | [[Recursive queries][Recursive queries]] | +#+END: + ** Milestones [[org-ql-search:todo%253A?super-groups=%2528%2528%253Aauto-property%2520%2522milestone%2522%2529%2529&sort=%2528priority%2529&title=%2522Milestones%2522][org-ql-search: Milestones]] @@ -63,7 +77,6 @@ - [[#document-sorters][Document sorters]] - [[#org-block-to-insert-results-of-queries-with-links-to-entries][Org block to insert results of queries with links to entries]] - [[#change-deadlines-auto-argument-to-auto-andor-auto-t][Change (deadline)'s auto argument to :auto and/or :auto t]] -- [[#save-views-to-dynamic-blocks][Save views to dynamic blocks]] - [[#consider-how-to-secure-searches-from-links-and-dynamic-blocks][Consider how to "secure" searches from links and dynamic blocks]] - [[#outline-path-in-buffers-files-arg][Outline path in buffers-files arg]] - [[#add-more-sorters][Add more sorters?]] @@ -75,11 +88,12 @@ - [[#update-view-screenshots][Update view screenshots]] - [[#org-agenda-skip-function][org-agenda-skip-function]] - [[#update-commentary][Update commentary]] -- [[#outline-path-predicate][Outline path predicate]] +- [[#fix-query-sexp-to-string-functions-handling-of-eg-descendants][Fix query-sexp-to-string function's handling of, e.g. descendants]] - [[#node-caching]["Node" caching]] - [[#overlay-based-caching-inspired-by-org-num-mode][Overlay-based caching inspired by org-num-mode]] - [[#alternative-parsing-libraries][Alternative parsing libraries]] - [[#fancier-searching-for-inherited-tags][Fancier searching for inherited tags]] +- [[#outline-path-predicate][Outline path predicate]] - [[#convert-simple-sexp-queries-to-non-sexp][Convert simple sexp queries to non-sexp]] - [[#tools-for-saving-queries-and-accessing-them-34][Tools for saving queries and accessing them {3/4}]] - [[#group-tag-support][Group tag support]] @@ -96,6 +110,7 @@ - [[#dual-matching-with-regexp-and-predicates][Dual matching with regexp and predicates]] - [[#operate-on-list-of-heading-positions][Operate on list of heading positions]] - [[#use-macros-for-date][Use macros for date]] +- [[#make-dynamic-blocks-warn-about-sexp-queries][Make dynamic blocks warn about sexp queries]] :END: ** TODO [#A] Add ~:auto~ keyword to ~(planning)~ predicate @@ -114,10 +129,6 @@ Note that the built-in sorting only works on Org elements, which is the default For consistency, because plain ~auto~ looks like a variable, and even though it's in a quoted form, it could be confusing. -** TODO [#A] Save views to dynamic blocks - -[2020-11-10 Tue 04:31] A command would save users from having to write out the dynamic block manually. - ** TODO [#A] Consider how to "secure" searches from links and dynamic blocks Theoretically one could put a sexp-based query into a link that would run arbitrary code to do something evil. Like: @@ -234,40 +245,11 @@ I should benchmark it to see how much difference it makes, because all those ~fs ** TODO [#C] Update commentary -** UNDERWAY [#A] Outline path predicate +** NEXT [#A] Fix query-sexp-to-string function's handling of, e.g. =descendants= :bug: -[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: +** NEXT [#A] Make dynamic blocks warn about sexp queries -#+BEGIN_SRC org - ,* Food - - ,** Fruits - - ,*** Blueberries - - ,*** Grapes - - ,** Vegetables - - ,*** Carrots - - ,*** Potatoes -#+END_SRC - -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 =Fruits= heading 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 [[id:6935361a-9e1d-48ec-8d17-876a90b90f50][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. +[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. ** UNDERWAY [#B] "Node" caching @@ -390,6 +372,41 @@ However, there might still be a useful idea here somewhere... 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] 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: + +#+BEGIN_SRC org + ,* Food + + ,** Fruits + + ,*** Blueberries + + ,*** Grapes + + ,** Vegetables + + ,*** Carrots + + ,*** Potatoes +#+END_SRC + +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 =Fruits= heading 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 [[id:6935361a-9e1d-48ec-8d17-876a90b90f50][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 [#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... @@ -1258,7 +1275,9 @@ Appears to be another implementation of magit-section-like expandable sections. :milestone: 0.6 :END: -[2020-11-12 Thu 03:23] A command to do this would be very helpful. +[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?) *** DONE [#A] Implement dynamic blocks From b90e6e25d0df92771e7f36ce8a4f39fad5d5cad8 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Fri, 13 Nov 2020 02:42:52 -0600 Subject: [PATCH 014/372] Tests: Use test buffer for link safety tests --- tests/test-org-ql.el | 144 +++++++++++++++++++++++-------------------- 1 file changed, 76 insertions(+), 68 deletions(-) diff --git a/tests/test-org-ql.el b/tests/test-org-ql.el index d31a08f..95519e8 100644 --- a/tests/test-org-ql.el +++ b/tests/test-org-ql.el @@ -1172,79 +1172,87 @@ RESULTS should be a list of strings as returned by (require 'org-ql-search) (require 'org-ql-view) - (cl-flet ((open-link - (link) (with-temp-buffer - (org-mode) - (insert link) - (backward-char 1) - (call-interactively #'org-open-at-point)))) + ;; The :auto-map test requires there to be results (because the org-super-agenda :auto-map group's + ;; key-form, where the safety check is, only gets evaluated when there are results). Using + ;; `with-temp-buffer' in the `open-link' function causes the temp buffer to be killed before the results + ;; have a chance to be gathered. So we make a test buffer and run the test in that, with a test heading. - (it "buffers-files parameter" - (let ((quoted-lambda-link "[[org-ql-search:todo:?buffers-files%3D%28lambda%20nil%20%28message%20%22AHA%22%29%29]]") - (unquoted-lambda-link "[[org-ql-search:todo:?buffers-files%3D%28lambda%20nil%20%28message%20%22AHA%22%29%29]]") - (quoted-lambda-in-list-link "[[org-ql-search:todo:?buffers-files%3D%28%28quote%20%28lambda%20nil%20%28message%20%22AHA%22%29%29%29%29]]") - (unquoted-lambda-in-list-link "[[org-ql-search:todo:?buffers-files%3D%28%28lambda%20nil%20%28message%20%22AHA%22%29%29%29]]")) - (expect (open-link quoted-lambda-link) - :to-throw 'wrong-type-argument '(stringp (lambda nil (message "AHA")))) - (expect (open-link unquoted-lambda-link) - :to-throw 'wrong-type-argument '(stringp (lambda nil (message "AHA")))) - (expect (open-link quoted-lambda-in-list-link) - :to-throw 'wrong-type-argument '(stringp ((quote (lambda nil (message "AHA")))))) - (expect (open-link unquoted-lambda-in-list-link) - :to-throw 'wrong-type-argument '(stringp ((lambda nil (message "AHA"))))))) + (let ((test-buffer (get-buffer-create "*test-org-ql*"))) + (cl-flet ((open-link + (link) (with-current-buffer test-buffer + (erase-buffer) + (org-mode) + (insert "* TODO Test heading \n\n") + (insert link) + (backward-char 1) + (call-interactively #'org-open-at-point)))) - (it "super-groups parameter" - (let ((quoted-lambda-link "[[org-ql-search:todo:?super-groups%3D%28lambda%20nil%20%28message%20%22AHA%22%29%29]]") - (unquoted-lambda-link "[[org-ql-search:todo:?super-groups%3D%28lambda%20nil%20%28message%20%22AHA%22%29%29]]") - (quoted-expression-link "[[org-ql-search:todo:?super-groups%3D%28message%20%22AHA%22%29]]") - (unquoted-expression-link "[[org-ql-search:todo:?super-groups%3D%22AHA%22]]") - (pred-selector-link "[[org-ql-search:todo:?super-groups%3D%28%28%3Apred%20%28lambda%20%28_%29%20%28message%20%22AHA%22%29%29%29%29]]") - (auto-map-selector-link "[[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]]")) - (expect (open-link quoted-lambda-link) - :to-throw 'wrong-type-argument '(listp lambda)) - (expect (open-link unquoted-lambda-link) - :to-throw 'wrong-type-argument '(listp lambda)) - (expect (open-link quoted-expression-link) - :to-throw 'wrong-type-argument '(listp message)) - (expect (open-link unquoted-expression-link) - :to-throw 'error '("cl-etypecase failed: AHA, (symbol list)")) + (it "buffers-files parameter" + (let ((quoted-lambda-link "[[org-ql-search:todo:?buffers-files%3D%28lambda%20nil%20%28message%20%22AHA%22%29%29]]") + (unquoted-lambda-link "[[org-ql-search:todo:?buffers-files%3D%28lambda%20nil%20%28message%20%22AHA%22%29%29]]") + (quoted-lambda-in-list-link "[[org-ql-search:todo:?buffers-files%3D%28%28quote%20%28lambda%20nil%20%28message%20%22AHA%22%29%29%29%29]]") + (unquoted-lambda-in-list-link "[[org-ql-search:todo:?buffers-files%3D%28%28lambda%20nil%20%28message%20%22AHA%22%29%29%29]]")) + (expect (open-link quoted-lambda-link) + :to-throw 'wrong-type-argument '(stringp (lambda nil (message "AHA")))) + (expect (open-link unquoted-lambda-link) + :to-throw 'wrong-type-argument '(stringp (lambda nil (message "AHA")))) + (expect (open-link quoted-lambda-in-list-link) + :to-throw 'wrong-type-argument '(stringp ((quote (lambda nil (message "AHA")))))) + (expect (open-link unquoted-lambda-in-list-link) + :to-throw 'wrong-type-argument '(stringp ((lambda nil (message "AHA"))))))) - ;; FIXME: These two tests will not pass (i.e. they will not - ;; signal an error) until the version of org-super-agenda in - ;; the test sandbox is upgraded, which I'll do when MELPA - ;; packages the latest version. On the bright side, I just - ;; confirmed that, without the fix to org-super-agenda, - ;; these are vulnerable, which means that the test catches - ;; this problem and detects the fix. - (expect (open-link pred-selector-link) - :to-throw 'error '("Unsafe groups disallowed (:pred): (lambda (_) (message AHA))")) - (expect (open-link auto-map-selector-link) - :to-throw 'error '("Unsafe groups disallowed (:auto-map): ((lambda (_) (message AHA)))")))) + (it "super-groups parameter" + (let ((quoted-lambda-link "[[org-ql-search:todo:?super-groups%3D%28lambda%20nil%20%28message%20%22AHA%22%29%29]]") + (unquoted-lambda-link "[[org-ql-search:todo:?super-groups%3D%28lambda%20nil%20%28message%20%22AHA%22%29%29]]") + (quoted-expression-link "[[org-ql-search:todo:?super-groups%3D%28message%20%22AHA%22%29]]") + (unquoted-expression-link "[[org-ql-search:todo:?super-groups%3D%22AHA%22]]") + (pred-selector-link "[[org-ql-search:todo:?super-groups%3D%28%28%3Apred%20%28lambda%20%28_%29%20%28message%20%22AHA%22%29%29%29%29]]") + (auto-map-selector-link "[[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]]")) + (expect (open-link quoted-lambda-link) + :to-throw 'wrong-type-argument '(listp lambda)) + (expect (open-link unquoted-lambda-link) + :to-throw 'wrong-type-argument '(listp lambda)) + (expect (open-link quoted-expression-link) + :to-throw 'wrong-type-argument '(listp message)) + (expect (open-link unquoted-expression-link) + :to-throw 'error '("cl-etypecase failed: AHA, (symbol list)")) - (it "title parameter" - (let ((quoted-lambda-link "[[org-ql-search:todo:?title%3D%28lambda%20%28_%20_%29%20%28message%20%22AHA%22%29%29]]") - (unquoted-lambda-link "[[org-ql-search:todo:?title%3D%28lambda%20%28_%20_%29%20%28message%20%22AHA%22%29%29]]") - (expression-link "[[org-ql-search:todo:?title%3D%28message%20%22AHA%22%29]]")) - (expect (open-link quoted-lambda-link) - :to-throw 'wrong-type-argument '(characterp lambda)) - (expect (open-link unquoted-lambda-link) - :to-throw 'wrong-type-argument '(characterp lambda)) - (expect (open-link expression-link) - :to-throw 'wrong-type-argument '(characterp message)))) + ;; FIXME: These two tests will not pass (i.e. they will not + ;; signal an error) until the version of org-super-agenda in + ;; the test sandbox is upgraded, which I'll do when MELPA + ;; packages the latest version. On the bright side, I just + ;; confirmed that, without the fix to org-super-agenda, + ;; these are vulnerable, which means that the test catches + ;; this problem and detects the fix. + (expect (open-link pred-selector-link) + :to-throw 'error '("Unsafe groups disallowed (:pred): (lambda (_) (message AHA))")) + (expect (open-link auto-map-selector-link) + :to-throw 'error '("Unsafe groups disallowed (:auto-map): ((lambda (_) (message AHA)))")))) - (it "sort parameter" - (let ((quoted-lambda-link "[[org-ql-search:todo:?sort%3D%28lambda%20%28_%20_%29%20%28message%20%22AHA%22%29%29]]") - (unquoted-lambda-link "[[org-ql-search:todo:?sort%3D%28lambda%20%28_%20_%29%20%28message%20%22AHA%22%29%29]]") - (quoted-lambda-in-list-link "[[org-ql-search:todo:?sort%3D%28%28quote%20%28lambda%20%28_%20_%29%20%28message%20%22AHA%22%29%29%29%29]]") - (unquoted-lambda-in-list-link "[[org-ql-search:todo:?sort=((lambda%20nil%20(message%20\"AHA\")))]]")) - (expect (open-link quoted-lambda-link) - :to-throw 'error '("Potentially unsafe value found in link’s SORT parameter ((lambda (_ _) (message AHA))). Link not opened")) - (expect (open-link unquoted-lambda-link) - :to-throw 'error '("Potentially unsafe value found in link’s SORT parameter ((lambda (_ _) (message AHA))). Link not opened")) - (expect (open-link quoted-lambda-in-list-link) - :to-throw 'error '("Potentially unsafe value found in link’s SORT parameter (((quote (lambda (_ _) (message AHA))))). Link not opened")) - (expect (open-link unquoted-lambda-in-list-link) - :to-throw 'error '("Potentially unsafe value found in link’s SORT parameter (((lambda nil (message AHA)))). Link not opened"))))))) + (it "title parameter" + (let ((quoted-lambda-link "[[org-ql-search:todo:?title%3D%28lambda%20%28_%20_%29%20%28message%20%22AHA%22%29%29]]") + (unquoted-lambda-link "[[org-ql-search:todo:?title%3D%28lambda%20%28_%20_%29%20%28message%20%22AHA%22%29%29]]") + (expression-link "[[org-ql-search:todo:?title%3D%28message%20%22AHA%22%29]]")) + (expect (open-link quoted-lambda-link) + :to-throw 'wrong-type-argument '(characterp lambda)) + (expect (open-link unquoted-lambda-link) + :to-throw 'wrong-type-argument '(characterp lambda)) + (expect (open-link expression-link) + :to-throw 'wrong-type-argument '(characterp message)))) + + (it "sort parameter" + (let ((quoted-lambda-link "[[org-ql-search:todo:?sort%3D%28lambda%20%28_%20_%29%20%28message%20%22AHA%22%29%29]]") + (unquoted-lambda-link "[[org-ql-search:todo:?sort%3D%28lambda%20%28_%20_%29%20%28message%20%22AHA%22%29%29]]") + (quoted-lambda-in-list-link "[[org-ql-search:todo:?sort%3D%28%28quote%20%28lambda%20%28_%20_%29%20%28message%20%22AHA%22%29%29%29%29]]") + (unquoted-lambda-in-list-link "[[org-ql-search:todo:?sort=((lambda%20nil%20(message%20\"AHA\")))]]")) + (expect (open-link quoted-lambda-link) + :to-throw 'error '("Potentially unsafe value found in link’s SORT parameter ((lambda (_ _) (message AHA))). Link not opened")) + (expect (open-link unquoted-lambda-link) + :to-throw 'error '("Potentially unsafe value found in link’s SORT parameter ((lambda (_ _) (message AHA))). Link not opened")) + (expect (open-link quoted-lambda-in-list-link) + :to-throw 'error '("Potentially unsafe value found in link’s SORT parameter (((quote (lambda (_ _) (message AHA))))). Link not opened")) + (expect (open-link unquoted-lambda-in-list-link) + :to-throw 'error '("Potentially unsafe value found in link’s SORT parameter (((lambda nil (message AHA)))). Link not opened")))))))) ;; Local Variables: ;; truncate-lines: t From f8b41ea0273cca8d72e4e0a40a6c9bd0671302bd Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Fri, 13 Nov 2020 02:44:46 -0600 Subject: [PATCH 015/372] Fix: (org-dblock-write:org-ql) Remove unused argument --- org-ql-search.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/org-ql-search.el b/org-ql-search.el index 2c4b213..a5f9b7b 100644 --- a/org-ql-search.el +++ b/org-ql-search.el @@ -295,7 +295,7 @@ For example, an org-ql dynamic block header could look like: (cons 'scheduled (lambda (element) (--when-let (org-element-property :scheduled element) (ts-format ts-format (ts-parse-org-element it))))) - (cons 'property (lambda (element property &optional value) + (cons 'property (lambda (element property) (org-element-property (intern (concat ":" (upcase property))) element))))) (elements (org-ql-query :from (current-buffer) :where query From 51f987c6b82934b66057513b26e49ccb59343424 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Fri, 13 Nov 2020 02:45:45 -0600 Subject: [PATCH 016/372] Fix: (org-dblock-write:org-ql) Rename unused variables --- org-ql-search.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/org-ql-search.el b/org-ql-search.el index a5f9b7b..59d57d5 100644 --- a/org-ql-search.el +++ b/org-ql-search.el @@ -310,9 +310,9 @@ For example, an org-ql dynamic block header could look like: collect (or (pcase-exhaustive column ((pred symbolp) (funcall (alist-get column format-fns) element)) - (`((,column . ,args) ,header) + (`((,column . ,args) ,_header) (apply (alist-get column format-fns) element args)) - (`(,column ,header) + (`(,column ,_header) (funcall (alist-get column format-fns) element))) "")) " | "))) From be4e941fc9d0efbfaff3bee90aa9a791b0e194fa Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Fri, 13 Nov 2020 03:13:48 -0600 Subject: [PATCH 017/372] Notes: Update --- notes.org | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/notes.org b/notes.org index 586045f..f883349 100644 --- a/notes.org +++ b/notes.org @@ -89,10 +89,12 @@ - [[#org-agenda-skip-function][org-agenda-skip-function]] - [[#update-commentary][Update commentary]] - [[#fix-query-sexp-to-string-functions-handling-of-eg-descendants][Fix query-sexp-to-string function's handling of, e.g. descendants]] +- [[#make-dynamic-blocks-warn-about-sexp-queries][Make dynamic blocks warn about sexp queries]] - [[#node-caching]["Node" caching]] - [[#overlay-based-caching-inspired-by-org-num-mode][Overlay-based caching inspired by org-num-mode]] - [[#alternative-parsing-libraries][Alternative parsing libraries]] - [[#fancier-searching-for-inherited-tags][Fancier searching for inherited tags]] +- [[#fix-org-ql-view--link-open-on-newer-org-versions][Fix org-ql-view--link-open on newer Org versions]] - [[#outline-path-predicate][Outline path predicate]] - [[#convert-simple-sexp-queries-to-non-sexp][Convert simple sexp queries to non-sexp]] - [[#tools-for-saving-queries-and-accessing-them-34][Tools for saving queries and accessing them {3/4}]] @@ -110,7 +112,6 @@ - [[#dual-matching-with-regexp-and-predicates][Dual matching with regexp and predicates]] - [[#operate-on-list-of-heading-positions][Operate on list of heading positions]] - [[#use-macros-for-date][Use macros for date]] -- [[#make-dynamic-blocks-warn-about-sexp-queries][Make dynamic blocks warn about sexp queries]] :END: ** TODO [#A] Add ~:auto~ keyword to ~(planning)~ predicate @@ -372,6 +373,21 @@ However, there might still be a useful idea here somewhere... 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] Fix =org-ql-view--link-open= on newer Org versions +:PROPERTIES: +:milestone: 0.5 +:END: + +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. + +*** NEXT [#A] Check Org release notes for link changes + ** PROJECT [#A] 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: From dd30f07942650fb1e058ac42eb78d880255246a0 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Sat, 14 Nov 2020 05:22:59 -0600 Subject: [PATCH 018/372] Notes: Update --- notes.org | 445 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 271 insertions(+), 174 deletions(-) diff --git a/notes.org b/notes.org index f883349..f0f0c59 100644 --- a/notes.org +++ b/notes.org @@ -1,4 +1,5 @@ #+PROPERTY: LOGGING nil +#+TODO: TODO MAYBE NEXT PROJECT UNDERWAY WAITING | DONE CANCELED * [#A] Contents :PROPERTIES: @@ -16,56 +17,66 @@ * [#A] Overview +** Bugs + ++ [[org-ql-search:todo%3A%20tags%3Abug?super-groups=%28%28%3Aauto-property%20%22Milestone%22%29%29&sort=%28priority%29&title=%22Bugs%22][org-ql-search: Bugs]] + +#+BEGIN: org-ql :query "todo: tags:bug" :columns ((priority "P") ((property "milestone") "M") (todo "Keyword") heading) :sort (priority date) :ts-format "%Y-%m-%d %H:%M" +| P | M | Keyword | Heading | +|---+---+---------+---------------------------------------------------------------------| +| A | | NEXT | [[Fix query-sexp-to-string function's handling of, e.g. =descendants=][Fix query-sexp-to-string function's handling of, e.g. =descendants=]] | +#+END: + ** Underway -[[org-ql-search:todo%253AUNDERWAY?sort=%2528priority%2529&title=%2522Underway%2522][org-ql-search: Underway]] ++ [[org-ql-search:todo%253AUNDERWAY?sort=%2528priority%2529&title=%2522Underway%2522][org-ql-search: Underway]] #+BEGIN: org-ql :query (todo "UNDERWAY") :columns ((priority "P") (todo "Keyword") heading) :sort (priority date) :ts-format "%Y-%m-%d %H:%M" | P | Keyword | Heading | |---+----------+--------------------------------------------------------------------| -| A | UNDERWAY | [[Outline path predicate][Outline path predicate]] | | B | UNDERWAY | [["Node" caching]["Node" caching]] | | | UNDERWAY | [[Benchmarking tags searches without and with new group-tags support][Benchmarking tags searches without and with new group-tags support]] | #+END: ** To-do -+ [[org-ql-search:todo:?super-groups=%2528%2528%253Aauto-outline-path%2529%2529&sort=%2528priority%2529][org-ql-search: Tasks]] ++ [[org-ql-search:todo%3A?super-groups=%28%28%3Atodo%20%22NEXT%22%29%20%28%3Atodo%20%22PROJECT%22%29%20%28%3Aauto-priority%29%29&sort=%28todo%29][org-ql-search: Tasks]] #+BEGIN: org-ql :query "todo: priority:A" :columns ((priority "P") todo heading) :sort (priority date) :take 7 -| P | Todo | Heading | -|---+----------+-----------------------------------------------------------------| -| A | TODO | [[Add ~:auto~ keyword to ~(planning)~ predicate][Add ~:auto~ keyword to ~(planning)~ predicate]] | -| A | TODO | [[Document sorters][Document sorters]] | -| A | TODO | [[Org block to insert results of queries with links to entries][Org block to insert results of queries with links to entries]] | -| A | TODO | [[Change ~(deadline)~'s ~auto~ argument to ~:auto~ and/or ~:auto t~][Change ~(deadline)~'s ~auto~ argument to ~:auto~ and/or ~:auto t~]] | -| A | TODO | [[Save views to dynamic blocks][Save views to dynamic blocks]] | -| A | TODO | [[Consider how to "secure" searches from links and dynamic blocks][Consider how to "secure" searches from links and dynamic blocks]] | -| A | UNDERWAY | [[Outline path predicate][Outline path predicate]] | +| P | Todo | Heading | +|---+---------+----------------------------------------------------------------------------| +| A | NEXT | [[Fix query-sexp-to-string function's handling of, e.g. =descendants=][Fix query-sexp-to-string function's handling of, e.g. =descendants=]] | +| A | NEXT | [[Make dynamic blocks warn about sexp queries][Make dynamic blocks warn about sexp queries]] | +| A | PROJECT | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | +| A | PROJECT | [[Fix =org-ql-view--link-open= on newer Org versions][Fix =org-ql-view--link-open= on newer Org versions]] | +| A | PROJECT | [[Convert simple sexp queries to non-sexp][Convert simple sexp queries to non-sexp]] | +| A | PROJECT | [[Checking links for unsafe parameters][Checking links for unsafe parameters]] | +| A | PROJECT | [[Org link types [2/3\]][Org link types [2/3]​]] | #+END: -** [[org-ql-search:(and%20(todo%20"PROJECT")%20(not%20(descendants%20(todo%20"NEXT"))))?super-groups=%2528%2528%253Aauto-outline-path%2529%2529&sort=%2528priority%2529&title=%2522Stuck%2520Projects%2522][Stuck projects]] +** [[org-ql-search:%28and%20%28todo%20%22PROJECT%22%29%20%28not%20%28descendants%20%28todo%20%22NEXT%22%29%29%29%29?super-groups=%28%28%3Aauto-parent%29%29&sort=%28priority%29&title=%22Stuck%20Projects%22][org-ql-search: Stuck Projects]] #+BEGIN: org-ql :query (and (todo "PROJECT") (not (descendants (todo "NEXT")))) :columns ((priority "P") heading) :sort (priority date) :take 7 -| P | Heading | -|---+---------------------------------------------------| -| A | [[Outline path predicate][Outline path predicate]] | -| A | [[Convert simple sexp queries to non-sexp][Convert simple sexp queries to non-sexp]] | -| A | [[Tools%20for%20saving%20queries%20and%20accessing%20them%20%5B3/4%5D][Tools for saving queries and accessing them {3/4}]] | -| A | [[Org%20link%20types%20%5B2/3%5D][Org link types {2/3}]] | -| A | [[Group tag support][Group tag support]] | -| A | [[Checking links for unsafe parameters][Checking links for unsafe parameters]] | -| B | [[Recursive queries][Recursive queries]] | +| P | Heading | +|---+----------------------------------------------------------------------------| +| A | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | +| A | [[Fix =org-ql-view--link-open= on newer Org versions][Fix =org-ql-view--link-open= on newer Org versions]] | +| A | [[Convert simple sexp queries to non-sexp][Convert simple sexp queries to non-sexp]] | +| A | [[Checking links for unsafe parameters][Checking links for unsafe parameters]] | +| A | [[Org link types [2/3\]][Org link types [2/3]​]] | +| B | [[Outline path predicate][Outline path predicate]] | +| B | [[Group tag support][Group tag support]] | #+END: ** Milestones -[[org-ql-search:todo%253A?super-groups=%2528%2528%253Aauto-property%2520%2522milestone%2522%2529%2529&sort=%2528priority%2529&title=%2522Milestones%2522][org-ql-search: Milestones]] +[[org-ql-search:todo%3A?super-groups=%28%28%3Aauto-property%20%22milestone%22%29%29&sort=%28todo%29&title=%22Milestones%22][org-ql-search: Milestones]] #+BEGIN: org-ql :query "todo: property:milestone" :columns (((property "milestone") "M") (priority "P") todo heading) :sort (priority date) :take 7 -| M | P | Todo | Heading | -|-----+---+------+----------------------------| -| 0.6 | B | TODO | [[Save view to dynamic block][Save view to dynamic block]] | +| M | P | Todo | Heading | +|-----+---+---------+----------------------------------------------------------------------------| +| 0.5 | A | PROJECT | [[Fix =org-ql-view--link-open= on newer Org versions][Fix =org-ql-view--link-open= on newer Org versions]] | +| 0.6 | B | PROJECT | [[Save view to dynamic block][Save view to dynamic block]] | #+END: * [#A] Tasks @@ -74,18 +85,12 @@ :END: :CONTENTS: - [[#add-auto-keyword-to-planning-predicate][Add :auto keyword to (planning) predicate]] -- [[#document-sorters][Document sorters]] -- [[#org-block-to-insert-results-of-queries-with-links-to-entries][Org block to insert results of queries with links to entries]] - [[#change-deadlines-auto-argument-to-auto-andor-auto-t][Change (deadline)'s auto argument to :auto and/or :auto t]] -- [[#consider-how-to-secure-searches-from-links-and-dynamic-blocks][Consider how to "secure" searches from links and dynamic blocks]] - [[#outline-path-in-buffers-files-arg][Outline path in buffers-files arg]] - [[#add-more-sorters][Add more sorters?]] - [[#default-sort][Default sort]] - [[#partial-match-for-property-queries][Partial match for property queries]] - [[#move-this-notes-file-into-an-orphan-metanotes-branch][Move this notes file into an orphan meta/notes branch]] -- [[#new-transient-transient-lisp-variable-class][New Transient transient-lisp-variable class]] -- [[#normalize-queries][Normalize queries]] -- [[#update-view-screenshots][Update view screenshots]] - [[#org-agenda-skip-function][org-agenda-skip-function]] - [[#update-commentary][Update commentary]] - [[#fix-query-sexp-to-string-functions-handling-of-eg-descendants][Fix query-sexp-to-string function's handling of, e.g. descendants]] @@ -94,16 +99,21 @@ - [[#overlay-based-caching-inspired-by-org-num-mode][Overlay-based caching inspired by org-num-mode]] - [[#alternative-parsing-libraries][Alternative parsing libraries]] - [[#fancier-searching-for-inherited-tags][Fancier searching for inherited tags]] +- [[#compatibility-with-org-94-custom-link-changes][Compatibility with Org 9.4 custom link changes]] - [[#fix-org-ql-view--link-open-on-newer-org-versions][Fix org-ql-view--link-open on newer Org versions]] -- [[#outline-path-predicate][Outline path predicate]] - [[#convert-simple-sexp-queries-to-non-sexp][Convert simple sexp queries to non-sexp]] -- [[#tools-for-saving-queries-and-accessing-them-34][Tools for saving queries and accessing them {3/4}]] -- [[#group-tag-support][Group tag support]] - [[#checking-links-for-unsafe-parameters][Checking links for unsafe parameters]] +- [[#outline-path-predicate][Outline path predicate]] +- [[#tools-for-saving-queries-and-accessing-them-34][Tools for saving queries and accessing them [3/4]​]] +- [[#group-tag-support][Group tag support]] +- [[#document-the-sorting-functions][Document the sorting functions]] - [[#recursive-queries][Recursive queries]] - [[#timeline-view][Timeline view]] - [[#implement-view-with-tabulated-list-mode-or-magit-section][Implement view with tabulated-list-mode or magit-section]] - [[#dynamic-blocks][Dynamic blocks]] +- [[#new-transient-transient-lisp-variable-class][New Transient transient-lisp-variable class]] +- [[#normalize-queries][Normalize queries]] +- [[#update-view-screenshots][Update view screenshots]] - [[#test-caching][Test caching]] - [[#helm-command][Helm command]] - [[#quickly-change-sortinggrouping-in-search-views][Quickly change sorting/grouping in search views]] @@ -114,32 +124,14 @@ - [[#use-macros-for-date][Use macros for date]] :END: -** TODO [#A] Add ~:auto~ keyword to ~(planning)~ predicate +** TODO [#B] Add ~:auto~ keyword to ~(planning)~ predicate It should act like ~(or (deadline auto) (scheduled :to today))~. -** TODO [#A] Document sorters - -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. - -** TODO [#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. - -** TODO [#A] Change ~(deadline)~'s ~auto~ argument to ~:auto~ and/or ~:auto t~ +** TODO [#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. -** TODO [#A] Consider how to "secure" searches from links and dynamic blocks - -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: - -1. For sexp-based queries in links and dynamic blocks, prompt for confirmation before running. -2. 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 ~and~ or something. A whitelist approach might be needed.) ** TODO [#B] Outline path in buffers-files arg :PROPERTIES: @@ -184,23 +176,6 @@ Searching that with a query like =property:author=Fabrice= returns nothing; the [2020-11-12 Thu 03:17] Will probably have to merge or delete some WIP branches first, otherwise they'll probably get conflicts. -** TODO [#C] [[https://github.com/magit/transient/issues/76][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. - -** TODO [#C] Normalize queries - -[2019-07-16 Tue 11:49] This serves two purposes: - -1. Equivalent queries will return the same results from the cache. -2. 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 =TODO= items, the ~(todo "TODO")~ selector would convert to a preamble that would quickly search through the file. But if there were a thousand =TODO= items, it wouldn't be as much of a benefit, and a ~(regexp "something")~ selector's preamble might be much faster, depending on how many times =something= appears 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. - -** TODO [#C] Update view screenshots - -e.g. doesn't currently show the =View= header. - ** TODO [#C] ~org-agenda-skip-function~ As discussed [[https://www.reddit.com/r/emacs/comments/cnrt2d/orgqlblock_integrates_orgql_into_org_agenda/ewi1q36/][here]], this is a cool feature that allows further integration into existing custom agenda commands. Example: @@ -247,6 +222,9 @@ I should benchmark it to see how much difference it makes, because all those ~fs ** TODO [#C] Update commentary ** NEXT [#A] Fix query-sexp-to-string function's handling of, e.g. =descendants= :bug: +:PROPERTIES: +:milestone: 0.5 +:END: ** NEXT [#A] Make dynamic blocks warn about sexp queries @@ -373,7 +351,27 @@ However, there might still be a useful idea here somewhere... 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] Fix =org-ql-view--link-open= on newer Org versions +** PROJECT [#A] Compatibility with Org 9.4 custom link changes :bug: +:PROPERTIES: +:milestone: 0.5 +:END: + +[2020-11-13 Fri 22:36] From [[https://www.orgmode.org/Changes.html][the changelog]]: + +#+BEGIN_QUOTE +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. +#+END_QUOTE + +Unfortunately it does not say what the new, required second argument is. + + +** PROJECT [#A] Fix =org-ql-view--link-open= on newer Org versions :compatibility:bug: :PROPERTIES: :milestone: 0.5 :END: @@ -386,42 +384,62 @@ I do recall something about links changing in Org 9.3 (or thereabouts), so that 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. -*** NEXT [#A] Check Org release notes for link changes +*** DONE [#A] Check Org release notes for link changes -** PROJECT [#A] Outline path predicate +[2020-11-13 Fri 22:44] From [[https://www.orgmode.org/Changes_old.html][the changelog]]: -[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: +#+BEGIN_QUOTE +Change bracket link escaping syntax -#+BEGIN_SRC org - ,* Food +Org used to percent-encode sensitive characters in the URI part of the bracket links. - ,** Fruits +Now, escaping mechanism uses the usual backslash character, according to the following rules, applied in order: - ,*** Blueberries +- 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. - ,*** Grapes +When in doubt, use the function org-link-escape in order to turn a link string into its properly escaped form. - ,** Vegetables +The following function will help switching your links to the new syntax: - ,*** Carrots +(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))))))))))) - ,*** Potatoes -#+END_SRC +The old org-link-escape and org-link-unescape functions have been renamed into org-link-encode and org-link-decode. +#+END_QUOTE -Matching could work like this: +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! -+ ~(outline "Food")~ :: Would return all nodes. -+ ~(outline "Fruits")~ :: Would return all fruits. +Is it even possible to support both Org 9.3+ and earlier versions at the same time? -Matching at a specific path would be something like: +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? -+ ~(outline-path "Food" "Fruits")~ :: Would return all fruits. But if there were another =Fruits= heading 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 [[id:6935361a-9e1d-48ec-8d17-876a90b90f50][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 [#A] Convert simple sexp queries to non-sexp @@ -525,7 +543,132 @@ To implement this with good performance probably needs an outline-path cache. I 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] Tools for saving queries and accessing them [3/4] +** PROJECT [#A] Checking links for unsafe parameters +:PROPERTIES: +:ID: ba70e375-eddb-40df-8892-fb418c1f70d1 +:END: +:LOGBOOK: +- State "PROJECT" from "UNDERWAY" [2020-11-12 Thu 00:26] +- State "UNDERWAY" from [2020-11-11 Wed 23:09] +:END: + +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: + +1. For sexp-based queries in links and dynamic blocks, prompt for confirmation before running. +2. 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 ~and~ or something. A whitelist approach might be needed.) + +*** TODO [#A] Tag org-super-agenda 1.2 and bump required version in org-ql +:PROPERTIES: +:milestone: 0.5 +:END: + +That /should/ force the version of org-super-agenda with the fix to be installed when org-ql is upgraded. + +*** DONE Add automated tests +:LOGBOOK: +- 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] +:END: + +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. + +*** DONE Enumerate and test parameters and potentially unsafe types +CLOSED: [2020-11-11 Wed 23:26] +:LOGBOOK: +- State "DONE" from "UNDERWAY" [2020-11-11 Wed 23:26] +- State "UNDERWAY" from [2020-11-11 Wed 23:15] +:END: + +#+CAPTION: Template for making testable links +#+BEGIN_SRC org +[[org-ql-search:todo:?]] +#+END_SRC + +#+CAPTION: Expression to insert encoded values into template (after the =?=) +#+BEGIN_SRC elisp + (insert (url-hexify-string (concat "buffers-files=" (prin1-to-string '((lambda () (message "AHA"))))))) +#+END_SRC + ++ [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] ~:pred~ selector (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-map= selector (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: + +1. 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: + + #+BEGIN_SRC elisp + (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")) + #+END_SRC + +2. Sexp queries already prompt for confirmation, unless the user has set =org-ql-view-ask-unsafe-links= to nil. + +[2020-11-11 Wed 23:27] That's all the parameters and all the types that I can think to test. + + +** 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: + +#+BEGIN_SRC org + ,* Food + + ,** Fruits + + ,*** Blueberries + + ,*** Grapes + + ,** Vegetables + + ,*** Carrots + + ,*** Potatoes +#+END_SRC + +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 =Fruits= heading 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 [[id:6935361a-9e1d-48ec-8d17-876a90b90f50][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=. @@ -685,7 +828,7 @@ That seems to work, like: *** DONE Save query from ql-agenda buffer -** PROJECT [#A] Group tag support +** PROJECT [#B] Group tag support *** UNDERWAY Benchmarking tags searches without and with new group-tags support @@ -937,82 +1080,10 @@ Next steps: + [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 =result= variable -** PROJECT [#A] Checking links for unsafe parameters -:PROPERTIES: -:ID: ba70e375-eddb-40df-8892-fb418c1f70d1 -:END: -:LOGBOOK: -- State "PROJECT" from "UNDERWAY" [2020-11-12 Thu 00:26] -- State "UNDERWAY" from [2020-11-11 Wed 23:09] -:END: +** PROJECT [#B] Document the sorting functions -*** TODO Tag org-super-agenda 1.2 and bump required version in org-ql +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. -That /should/ force the version of org-super-agenda with the fix to be installed when org-ql is upgraded. - -*** DONE Add automated tests -:LOGBOOK: -- 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] -:END: - -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. - -*** DONE Enumerate and test parameters and potentially unsafe types -CLOSED: [2020-11-11 Wed 23:26] -:LOGBOOK: -- State "DONE" from "UNDERWAY" [2020-11-11 Wed 23:26] -- State "UNDERWAY" from [2020-11-11 Wed 23:15] -:END: - -#+CAPTION: Template for making testable links -#+BEGIN_SRC org -[[org-ql-search:todo:?]] -#+END_SRC - -#+CAPTION: Expression to insert encoded values into template (after the =?=) -#+BEGIN_SRC elisp - (insert (url-hexify-string (concat "buffers-files=" (prin1-to-string '((lambda () (message "AHA"))))))) -#+END_SRC - -+ [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] ~:pred~ selector (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-map= selector (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: - -1. 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: - - #+BEGIN_SRC elisp - (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")) - #+END_SRC - -2. Sexp queries already prompt for confirmation, unless the user has set =org-ql-view-ask-unsafe-links= to nil. - -[2020-11-11 Wed 23:27] That's all the parameters and all the types that I can think to test. ** PROJECT [#B] Recursive queries @@ -1286,7 +1357,7 @@ Appears to be another implementation of magit-section-like expandable sections. ** PROJECT Dynamic blocks -*** TODO [#B] Save view to dynamic block +*** PROJECT [#B] Save view to dynamic block :PROPERTIES: :milestone: 0.6 :END: @@ -1374,6 +1445,32 @@ For example, [[https://egli.dev/posts/using-org-mode-for-meeting-minutes/][this [2020-11-10 Tue 00:03] I think it's ready to merge now. +*** DONE [#A] Org block to insert results of queries with links to entries +:PROPERTIES: +:ID: 422754bb-3a7c-4dbf-b303-4056d3cafb7e +:END: + +[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 [#C] [[https://github.com/magit/transient/issues/76][New Transient transient-lisp-variable class]] :compatibility: + +[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: + +1. Equivalent queries will return the same results from the cache. +2. 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 =TODO= items, the ~(todo "TODO")~ selector would convert to a preamble that would quickly search through the file. But if there were a thousand =TODO= items, it wouldn't be as much of a benefit, and a ~(regexp "something")~ selector's preamble might be much faster, depending on how many times =something= appears 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. @@ -1940,7 +2037,7 @@ Also, maybe instead of having a single =date= selector, I should have =scheduled :END: :CONTENTS: -- [[#2019-08-29-thu-0624--benchmarking-org-ql-compared-to-re-search-forward-for-getting-headings-in-buffer][{2019-08-29 Thu 06:24} Benchmarking org-ql compared to re-search-forward for getting headings in buffer]] +- [[#2019-08-29-thu-0624--benchmarking-org-ql-compared-to-re-search-forward-for-getting-headings-in-buffer][[2019-08-29 Thu 06:24] Benchmarking org-ql compared to re-search-forward for getting headings in buffer]] - [[#caching-of-inherited-tags][Caching of inherited tags]] - [[#intersecting-query-results][Intersecting query results]] - [[#more-profiling][More profiling]] From 89ff02a1501b53b4e20cdc66a8fcaa37e7d15734 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Sat, 14 Nov 2020 05:40:24 -0600 Subject: [PATCH 019/372] Fix: (org-ql--query-sexp-to-string) Ancestors, etc. queries --- org-ql.el | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/org-ql.el b/org-ql.el index dc6ee24..f75ce4c 100644 --- a/org-ql.el +++ b/org-ql.el @@ -1778,7 +1778,11 @@ Multiple predicates are combined with BOOLEAN." If QUERY can't be converted to a string, 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))) + (or (contains-p 'or query) + (contains-p 'ancestors query) + (contains-p 'children query) + (contains-p 'descendants query) + (contains-p 'parent query))) (contains-p (symbol list) (cl-loop for element in list thereis (or (eq symbol element) From 0365b785cc075db0012bc919ef101f0b43ba9446 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Sat, 14 Nov 2020 05:41:37 -0600 Subject: [PATCH 020/372] Fix: (org-ql--defpred) Add to predicates variable when compiling This seems to fix an occasional weird bug encountered when byte-compiling and loading this file. I can't explain why it only happens sometimes, but this seems to fix it, and it stands to reason why it does, so it seems like the right thing to do. --- org-ql.el | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/org-ql.el b/org-ql.el index f75ce4c..3825ec2 100644 --- a/org-ql.el +++ b/org-ql.el @@ -159,7 +159,11 @@ match." (fn-name (intern (concat "org-ql--predicate-" (symbol-name name)))) (pred-name (intern (symbol-name name)))) `(progn - (push (list :name ',pred-name :aliases ',aliases :fn ',fn-name :docstring ,docstring :args ',args) org-ql-predicates) + (cl-eval-when (compile load eval) + ;; When compiling, the predicate must be added to `org-ql-predicates' before `org-ql--def-plain-query-fn' + ;; is called to define `org-ql--plain-query'. Otherwise, `org-ql--plain-query' seems to work properly + ;; when interpreted but not always when the file is byte-compiled. + (push (list :name ',pred-name :aliases ',aliases :fn ',fn-name :docstring ,docstring :args ',args) org-ql-predicates)) (cl-defun ,fn-name ,args ,docstring ,@body)))) ;; TODO: Mark as obsolete/deprecated. From 4a8deb541e9eb70afd54cbe1f4dbe29f375cdf4a Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Sat, 14 Nov 2020 05:43:12 -0600 Subject: [PATCH 021/372] Comment: Add FIXME --- org-ql-view.el | 1 + 1 file changed, 1 insertion(+) diff --git a/org-ql-view.el b/org-ql-view.el index 843211e..f915591 100644 --- a/org-ql-view.el +++ b/org-ql-view.el @@ -683,6 +683,7 @@ When opened, the link searches the buffer it's opened from." "?" (url-build-query-string (delete nil params)))) (url (url-recreate-url (url-parse-make-urlobj "org-ql-search" nil nil nil nil filename)))) + ;; FIXME: "Warning: ‘org-store-link-props’ is an obsolete function (as of Org 9.3); use ‘org-link-store-props’ instead" (org-store-link-props :type "org-ql-search" :link url From 1b12d4e9b0c5d4a5839434e80875f96f51d547ad Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Sat, 14 Nov 2020 05:43:50 -0600 Subject: [PATCH 022/372] Fix: (org-ql-view--link-store) Don't re-format formatted queries org-ql-view--format-query already returns a string. --- org-ql-view.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/org-ql-view.el b/org-ql-view.el index f915591..cba3ad9 100644 --- a/org-ql-view.el +++ b/org-ql-view.el @@ -669,7 +669,7 @@ When opened, the link searches the buffer it's opened from." (cl-every #'string-or-file-buffer-p org-ql-view-buffers-files))) (user-error "Views that search non-file-backed buffers can't be linked to")) (let* ((query-string (--if-let (org-ql--query-sexp-to-string org-ql-view-query) - it (prin1-to-string (org-ql-view--format-query org-ql-view-query)))) + it (org-ql-view--format-query org-ql-view-query))) (buffers-files (prompt-for (org-ql-view--contract-buffers-files org-ql-view-buffers-files))) (params (list (when buffers-files (list "buffers-files" buffers-files)) From 0a2b75146469074e36673957444935b9c68d27f8 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Sat, 14 Nov 2020 20:35:52 -0600 Subject: [PATCH 023/372] Fix: (org-ql-view-bookmark-make-record) List of filenames Fixes #148. Thanks to @tpeacock19 for reporting. --- org-ql-view.el | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/org-ql-view.el b/org-ql-view.el index cba3ad9..e4ac4c5 100644 --- a/org-ql-view.el +++ b/org-ql-view.el @@ -544,10 +544,12 @@ dates in the past, and negative for dates in the future." (defun org-ql-view-bookmark-make-record () "Return a bookmark record for the current Org QL View buffer." (cl-labels ((file-nameize - (b-f) (or (buffer-file-name b-f) - (when (buffer-base-buffer b-f) - (buffer-file-name (buffer-base-buffer b-f))) - (user-error "Only file-backed buffers can be bookmarked by Org QL View: %s" b-f)))) + (b-f) (cl-typecase b-f + (string b-f) + (buffer (or (buffer-file-name b-f) + (when (buffer-base-buffer b-f) + (buffer-file-name (buffer-base-buffer b-f))))) + (t (user-error "Only file-backed buffers can be bookmarked by Org QL View: %s" b-f))))) (pcase-let* ((plist (org-ql-view--plist (current-buffer))) ((map :buffers-files) plist)) ;; Replace buffers with their filenames, and signal error if any are not file-backed. From 5b401cd8b79086f6cdcca0bd22ef4267efa80851 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Sat, 14 Nov 2020 20:36:28 -0600 Subject: [PATCH 024/372] Tidy: (org-ql-view-bookmark-make-record) --- org-ql-view.el | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/org-ql-view.el b/org-ql-view.el index e4ac4c5..f5f7fb7 100644 --- a/org-ql-view.el +++ b/org-ql-view.el @@ -557,8 +557,7 @@ dates in the past, and negative for dates in the future." (cl-etypecase buffers-files (string buffers-files) (buffer (file-nameize buffers-files)) - (list (cl-loop for b-f in buffers-files - collect (file-nameize b-f)))))) + (list (mapcar #'file-nameize buffers-files))))) (list (concat "Org QL View: " org-ql-view-title) (cons 'org-ql-view-plist plist) (cons 'handler #'org-ql-view-bookmark-handler) From 60d0c2a45589bfeaccc0096dbb80ee13b32c5f24 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Sat, 14 Nov 2020 20:53:30 -0600 Subject: [PATCH 025/372] Notes: Update --- notes.org | 139 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 79 insertions(+), 60 deletions(-) diff --git a/notes.org b/notes.org index f0f0c59..48f914a 100644 --- a/notes.org +++ b/notes.org @@ -17,66 +17,63 @@ * [#A] Overview -** Bugs +** Bugs ([[org-ql-search:todo%253A%2520tags%253Abug?super-groups=%2528%2528%253Aauto-property%2520%2522Milestone%2522%2529%2529&sort=%2528todo%2529&title=%2522Bugs%2522][view]]) -+ [[org-ql-search:todo%3A%20tags%3Abug?super-groups=%28%28%3Aauto-property%20%22Milestone%22%29%29&sort=%28priority%29&title=%22Bugs%22][org-ql-search: Bugs]] - -#+BEGIN: org-ql :query "todo: tags:bug" :columns ((priority "P") ((property "milestone") "M") (todo "Keyword") heading) :sort (priority date) :ts-format "%Y-%m-%d %H:%M" -| P | M | Keyword | Heading | -|---+---+---------+---------------------------------------------------------------------| -| A | | NEXT | [[Fix query-sexp-to-string function's handling of, e.g. =descendants=][Fix query-sexp-to-string function's handling of, e.g. =descendants=]] | +#+BEGIN: org-ql :query "todo: tags:bug" :columns ((priority "P") ((property "milestone") "M") (todo "Keyword") heading) :sort (priority todo date) :ts-format "%Y-%m-%d %H:%M" +| P | M | Keyword | Heading | +|---+-----+---------+------------------------------------------------| +| A | 0.5 | NEXT | [[Write tests for storing/opening links][Write tests for storing/opening links]] | +| A | 0.5 | PROJECT | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | +| A | 0.5 | PROJECT | [[Fix =org-ql-view--link-open= on Org 9.3+][Fix =org-ql-view--link-open= on Org 9.3+]] | #+END: -** Underway +** Underway ([[org-ql-search:todo%253AUNDERWAY?sort=%2528priority%2529&title=%2522Underway%2522][view]]) -+ [[org-ql-search:todo%253AUNDERWAY?sort=%2528priority%2529&title=%2522Underway%2522][org-ql-search: Underway]] - -#+BEGIN: org-ql :query (todo "UNDERWAY") :columns ((priority "P") (todo "Keyword") heading) :sort (priority date) :ts-format "%Y-%m-%d %H:%M" -| P | Keyword | Heading | -|---+----------+--------------------------------------------------------------------| -| B | UNDERWAY | [["Node" caching]["Node" caching]] | -| | UNDERWAY | [[Benchmarking tags searches without and with new group-tags support][Benchmarking tags searches without and with new group-tags support]] | +#+BEGIN: org-ql :query (todo "UNDERWAY") :columns (((property "milestone") "M") (priority "P") (todo "Keyword") heading) :sort (priority date) :ts-format "%Y-%m-%d %H:%M" +| M | P | Keyword | Heading | +|---+---+----------+--------------------------------------------------------------------| +| | B | UNDERWAY | [["Node" caching]["Node" caching]] | +| | | UNDERWAY | [[Benchmarking tags searches without and with new group-tags support][Benchmarking tags searches without and with new group-tags support]] | #+END: -** To-do +** To-do ([[org-ql-search:todo:?super-groups=((:todo%20"NEXT")%20(:todo%20"PROJECT")%20(:auto-priority))&sort=(todo)][view]]) -+ [[org-ql-search:todo%3A?super-groups=%28%28%3Atodo%20%22NEXT%22%29%20%28%3Atodo%20%22PROJECT%22%29%20%28%3Aauto-priority%29%29&sort=%28todo%29][org-ql-search: Tasks]] - -#+BEGIN: org-ql :query "todo: priority:A" :columns ((priority "P") todo heading) :sort (priority date) :take 7 -| P | Todo | Heading | -|---+---------+----------------------------------------------------------------------------| -| A | NEXT | [[Fix query-sexp-to-string function's handling of, e.g. =descendants=][Fix query-sexp-to-string function's handling of, e.g. =descendants=]] | -| A | NEXT | [[Make dynamic blocks warn about sexp queries][Make dynamic blocks warn about sexp queries]] | -| A | PROJECT | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | -| A | PROJECT | [[Fix =org-ql-view--link-open= on newer Org versions][Fix =org-ql-view--link-open= on newer Org versions]] | -| A | PROJECT | [[Convert simple sexp queries to non-sexp][Convert simple sexp queries to non-sexp]] | -| A | PROJECT | [[Checking links for unsafe parameters][Checking links for unsafe parameters]] | -| A | PROJECT | [[Org link types [2/3\]][Org link types [2/3]​]] | +#+BEGIN: org-ql :query "todo: priority:A" :columns ((priority "P") ((property "milestone") "M") todo heading) :sort (priority date) :take 7 +| P | M | Todo | Heading | +|---+-----+---------+--------------------------------------------------------------| +| A | | NEXT | [[Make dynamic blocks warn about sexp queries][Make dynamic blocks warn about sexp queries]] | +| A | 0.5 | PROJECT | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | +| A | 0.5 | PROJECT | [[Fix =org-ql-view--link-open= on Org 9.3+][Fix =org-ql-view--link-open= on Org 9.3+]] | +| A | 0.5 | NEXT | [[Write tests for storing/opening links][Write tests for storing/opening links]] | +| A | | PROJECT | [[Convert simple sexp queries to non-sexp][Convert simple sexp queries to non-sexp]] | +| A | | PROJECT | [[Checking links for unsafe parameters][Checking links for unsafe parameters]] | +| A | 0.5 | TODO | [[Tag org-super-agenda 1.2 and bump required version in org-ql][Tag org-super-agenda 1.2 and bump required version in org-ql]] | #+END: -** [[org-ql-search:%28and%20%28todo%20%22PROJECT%22%29%20%28not%20%28descendants%20%28todo%20%22NEXT%22%29%29%29%29?super-groups=%28%28%3Aauto-parent%29%29&sort=%28priority%29&title=%22Stuck%20Projects%22][org-ql-search: Stuck Projects]] +** Stuck projects ([[org-ql-search:(and%20(todo%20"PROJECT")%20(not%20(descendants%20(todo%20"NEXT"))))?super-groups=((:auto-parent))&sort=(priority)&title="Stuck%20Projects"][view]]) -#+BEGIN: org-ql :query (and (todo "PROJECT") (not (descendants (todo "NEXT")))) :columns ((priority "P") heading) :sort (priority date) :take 7 -| P | Heading | -|---+----------------------------------------------------------------------------| -| A | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | -| A | [[Fix =org-ql-view--link-open= on newer Org versions][Fix =org-ql-view--link-open= on newer Org versions]] | -| A | [[Convert simple sexp queries to non-sexp][Convert simple sexp queries to non-sexp]] | -| A | [[Checking links for unsafe parameters][Checking links for unsafe parameters]] | -| A | [[Org link types [2/3\]][Org link types [2/3]​]] | -| B | [[Outline path predicate][Outline path predicate]] | -| B | [[Group tag support][Group tag support]] | +#+BEGIN: org-ql :query (and (todo "PROJECT") (not (descendants (todo "NEXT")))) :columns (((property "milestone") "M") (priority "P") heading) :sort (priority date) :take 7 +| M | P | Heading | +|-----+---+-------------------------------------------------------| +| 0.5 | A | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | +| | A | [[Convert simple sexp queries to non-sexp][Convert simple sexp queries to non-sexp]] | +| | A | [[Checking links for unsafe parameters][Checking links for unsafe parameters]] | +| | A | [[Org%20link%20types%20%5B2/3%5D][Org link types {2/3}]] | +| | B | [[Move this notes file into an orphan =meta/notes= branch][Move this notes file into an orphan =meta/notes= branch]] | +| | B | [[Outline path predicate][Outline path predicate]] | +| | B | [[Group tag support][Group tag support]] | #+END: -** Milestones - -[[org-ql-search:todo%3A?super-groups=%28%28%3Aauto-property%20%22milestone%22%29%29&sort=%28todo%29&title=%22Milestones%22][org-ql-search: Milestones]] +** Milestones ([[org-ql-search:todo:?super-groups=((:auto-property%20"milestone"))&sort=(todo)&title="Milestones"][view]]) #+BEGIN: org-ql :query "todo: property:milestone" :columns (((property "milestone") "M") (priority "P") todo heading) :sort (priority date) :take 7 -| M | P | Todo | Heading | -|-----+---+---------+----------------------------------------------------------------------------| -| 0.5 | A | PROJECT | [[Fix =org-ql-view--link-open= on newer Org versions][Fix =org-ql-view--link-open= on newer Org versions]] | -| 0.6 | B | PROJECT | [[Save view to dynamic block][Save view to dynamic block]] | +| M | P | Todo | Heading | +|-----+---+---------+--------------------------------------------------------------| +| 0.5 | A | PROJECT | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | +| 0.5 | A | PROJECT | [[Fix =org-ql-view--link-open= on Org 9.3+][Fix =org-ql-view--link-open= on Org 9.3+]] | +| 0.5 | A | NEXT | [[Write tests for storing/opening links][Write tests for storing/opening links]] | +| 0.5 | A | TODO | [[Tag org-super-agenda 1.2 and bump required version in org-ql][Tag org-super-agenda 1.2 and bump required version in org-ql]] | +| 0.6 | B | PROJECT | [[Save view to dynamic block][Save view to dynamic block]] | #+END: * [#A] Tasks @@ -93,18 +90,17 @@ - [[#move-this-notes-file-into-an-orphan-metanotes-branch][Move this notes file into an orphan meta/notes branch]] - [[#org-agenda-skip-function][org-agenda-skip-function]] - [[#update-commentary][Update commentary]] -- [[#fix-query-sexp-to-string-functions-handling-of-eg-descendants][Fix query-sexp-to-string function's handling of, e.g. descendants]] - [[#make-dynamic-blocks-warn-about-sexp-queries][Make dynamic blocks warn about sexp queries]] - [[#node-caching]["Node" caching]] - [[#overlay-based-caching-inspired-by-org-num-mode][Overlay-based caching inspired by org-num-mode]] - [[#alternative-parsing-libraries][Alternative parsing libraries]] - [[#fancier-searching-for-inherited-tags][Fancier searching for inherited tags]] - [[#compatibility-with-org-94-custom-link-changes][Compatibility with Org 9.4 custom link changes]] -- [[#fix-org-ql-view--link-open-on-newer-org-versions][Fix org-ql-view--link-open on newer Org versions]] +- [[#fix-org-ql-view--link-open-on-org-93][Fix org-ql-view--link-open on Org 9.3+]] - [[#convert-simple-sexp-queries-to-non-sexp][Convert simple sexp queries to non-sexp]] - [[#checking-links-for-unsafe-parameters][Checking links for unsafe parameters]] - [[#outline-path-predicate][Outline path predicate]] -- [[#tools-for-saving-queries-and-accessing-them-34][Tools for saving queries and accessing them [3/4]​]] +- [[#tools-for-saving-queries-and-accessing-them-34][Tools for saving queries and accessing them {3/4}]] - [[#group-tag-support][Group tag support]] - [[#document-the-sorting-functions][Document the sorting functions]] - [[#recursive-queries][Recursive queries]] @@ -115,6 +111,7 @@ - [[#normalize-queries][Normalize queries]] - [[#update-view-screenshots][Update view screenshots]] - [[#test-caching][Test caching]] +- [[#fix-query-sexp-to-string-functions-handling-of-eg-descendants][Fix query-sexp-to-string function's handling of, e.g. descendants]] - [[#helm-command][Helm command]] - [[#quickly-change-sortinggrouping-in-search-views][Quickly change sorting/grouping in search views]] - [[#byte-compile-lambdas][Byte-compile lambdas]] @@ -172,10 +169,6 @@ e.g. something like [[https://200ok.ch/posts/2020-02-09_creating_org_mode_sparse 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. -** TODO [#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. - ** TODO [#C] ~org-agenda-skip-function~ As discussed [[https://www.reddit.com/r/emacs/comments/cnrt2d/orgqlblock_integrates_orgql_into_org_agenda/ewi1q36/][here]], this is a cool feature that allows further integration into existing custom agenda commands. Example: @@ -221,11 +214,6 @@ I should benchmark it to see how much difference it makes, because all those ~fs ** TODO [#C] Update commentary -** NEXT [#A] Fix query-sexp-to-string function's handling of, e.g. =descendants= :bug: -:PROPERTIES: -:milestone: 0.5 -:END: - ** NEXT [#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. @@ -371,7 +359,7 @@ Eventually, the function org-export-custom-protocol-maybe is now called with a f Unfortunately it does not say what the new, required second argument is. -** PROJECT [#A] Fix =org-ql-view--link-open= on newer Org versions :compatibility:bug: +** PROJECT [#A] Fix =org-ql-view--link-open= on Org 9.3+ :compatibility:bug: :PROPERTIES: :milestone: 0.5 :END: @@ -384,6 +372,26 @@ I do recall something about links changing in Org 9.3 (or thereabouts), so that 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=: + +#+BEGIN_SRC elisp + (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))) +#+END_SRC + +But, first, I should write tests for saving and opening links, so it can actually be tested on different versions automatically. + +*** NEXT [#A] Write tests for storing/opening links +:PROPERTIES: +:milestone: 0.5 +:END: + *** DONE [#A] Check Org release notes for link changes [2020-11-13 Fri 22:44] From [[https://www.orgmode.org/Changes_old.html][the changelog]]: @@ -633,6 +641,10 @@ For the query expression: [2020-11-11 Wed 23:27] That's all the parameters and all the types that I can think to test. +** PROJECT [#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. + ** 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: @@ -1475,6 +1487,13 @@ e.g. doesn't currently show the =View= header. 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. +** DONE [#A] Fix query-sexp-to-string function's handling of, e.g. =descendants= :bug: +:PROPERTIES: +:milestone: 0.5 +:END: + +[2020-11-14 Sat 20:45] Fixed in =89ff02a1501b53b4e20cdc66a8fcaa37e7d15734=. + ** DONE [#A] Helm command In branch =wip/helm-org-ql=. Works really well, should add it and demonstrate it. @@ -2037,7 +2056,7 @@ Also, maybe instead of having a single =date= selector, I should have =scheduled :END: :CONTENTS: -- [[#2019-08-29-thu-0624--benchmarking-org-ql-compared-to-re-search-forward-for-getting-headings-in-buffer][[2019-08-29 Thu 06:24] Benchmarking org-ql compared to re-search-forward for getting headings in buffer]] +- [[#2019-08-29-thu-0624--benchmarking-org-ql-compared-to-re-search-forward-for-getting-headings-in-buffer][{2019-08-29 Thu 06:24} Benchmarking org-ql compared to re-search-forward for getting headings in buffer]] - [[#caching-of-inherited-tags][Caching of inherited tags]] - [[#intersecting-query-results][Intersecting query results]] - [[#more-profiling][More profiling]] From 511edbfff85618be847dc992a17b6ff8b4405cfb Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Sat, 14 Nov 2020 20:58:19 -0600 Subject: [PATCH 026/372] Notes: Make ToC after sorting --- notes.org | 2 +- org-ql-view.el | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/notes.org b/notes.org index 48f914a..223a652 100644 --- a/notes.org +++ b/notes.org @@ -4184,6 +4184,6 @@ I don't need this right now, but it might come in handy here or elsewhere. ** File-local variables # Local Variables: -# before-save-hook: (org-make-toc (lambda () (when (fboundp 'unpackaged/org-fix-blank-lines) (unpackaged/org-fix-blank-lines t))) (lambda () (when (fboundp 'ap/org-sort-entries-recursive-multi) (save-excursion (goto-char (point-min)) (ap/org-sort-entries-recursive-multi '(?a ?p ?o)))))) +# before-save-hook: ((lambda () (when (fboundp 'unpackaged/org-fix-blank-lines) (unpackaged/org-fix-blank-lines t))) (lambda () (when (fboundp 'ap/org-sort-entries-recursive-multi) (save-excursion (goto-char (point-min)) (ap/org-sort-entries-recursive-multi '(?a ?p ?o))))) org-make-toc) # End: diff --git a/org-ql-view.el b/org-ql-view.el index f5f7fb7..0101a69 100644 --- a/org-ql-view.el +++ b/org-ql-view.el @@ -662,9 +662,10 @@ When opened, the link searches the buffer it's opened from." ("containing file" nil) (buffers-files (prin1-to-string buffers-files)))) (string-or-file-buffer-p - (thing) (or (stringp thing) - (and (bufferp thing) - (buffer-file-name thing))))) + (thing) (cl-typecase thing + (string thing) + (buffer (or (buffer-file-name thing) + (buffer-file-name (buffer-base-buffer thing))))))) (unless (or (string-or-file-buffer-p org-ql-view-buffers-files) (and (listp org-ql-view-buffers-files) (cl-every #'string-or-file-buffer-p org-ql-view-buffers-files))) From 5c8260b2721b29d78892c35691c9218d55d981c4 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Sat, 14 Nov 2020 20:58:32 -0600 Subject: [PATCH 027/372] Notes: Add checklist --- notes.org | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/notes.org b/notes.org index 223a652..b02ccae 100644 --- a/notes.org +++ b/notes.org @@ -1855,6 +1855,18 @@ If I made the =date= selector a macro, I could avoid the need to quote the compa Also, maybe instead of having a single =date= selector, I should have =scheduled=, =deadline=, etc. +* Checklists + +** Commit + +*** Fix + ++ [ ] Check repo for closing issue ++ [ ] Check this file for closing task ++ [ ] Lint ++ [ ] Test ++ [ ] Commit and push + * Examples / testing :PROPERTIES: :TOC: :include descendants :depth 1 From dabb812a3c221d28bc6244032404aea8d3eeeb1d Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Sat, 14 Nov 2020 20:58:45 -0600 Subject: [PATCH 028/372] Notes: Update --- notes.org | 224 +++++++++++++++++++++++++++--------------------------- 1 file changed, 113 insertions(+), 111 deletions(-) diff --git a/notes.org b/notes.org index b02ccae..ebfa4a9 100644 --- a/notes.org +++ b/notes.org @@ -8,6 +8,7 @@ :CONTENTS: - [[#overview][Overview]] - [[#tasks][Tasks]] +- [[#checklists][Checklists]] - [[#examples--testing][Examples / testing]] - [[#in-the-wild][In the wild]] - [[#profiling][Profiling]] @@ -87,18 +88,17 @@ - [[#add-more-sorters][Add more sorters?]] - [[#default-sort][Default sort]] - [[#partial-match-for-property-queries][Partial match for property queries]] -- [[#move-this-notes-file-into-an-orphan-metanotes-branch][Move this notes file into an orphan meta/notes branch]] - [[#org-agenda-skip-function][org-agenda-skip-function]] - [[#update-commentary][Update commentary]] -- [[#make-dynamic-blocks-warn-about-sexp-queries][Make dynamic blocks warn about sexp queries]] -- [[#node-caching]["Node" caching]] - [[#overlay-based-caching-inspired-by-org-num-mode][Overlay-based caching inspired by org-num-mode]] - [[#alternative-parsing-libraries][Alternative parsing libraries]] - [[#fancier-searching-for-inherited-tags][Fancier searching for inherited tags]] +- [[#make-dynamic-blocks-warn-about-sexp-queries][Make dynamic blocks warn about sexp queries]] - [[#compatibility-with-org-94-custom-link-changes][Compatibility with Org 9.4 custom link changes]] - [[#fix-org-ql-view--link-open-on-org-93][Fix org-ql-view--link-open on Org 9.3+]] - [[#convert-simple-sexp-queries-to-non-sexp][Convert simple sexp queries to non-sexp]] - [[#checking-links-for-unsafe-parameters][Checking links for unsafe parameters]] +- [[#move-this-notes-file-into-an-orphan-metanotes-branch][Move this notes file into an orphan meta/notes branch]] - [[#outline-path-predicate][Outline path predicate]] - [[#tools-for-saving-queries-and-accessing-them-34][Tools for saving queries and accessing them {3/4}]] - [[#group-tag-support][Group tag support]] @@ -111,6 +111,7 @@ - [[#normalize-queries][Normalize queries]] - [[#update-view-screenshots][Update view screenshots]] - [[#test-caching][Test caching]] +- [[#node-caching]["Node" caching]] - [[#fix-query-sexp-to-string-functions-handling-of-eg-descendants][Fix query-sexp-to-string function's handling of, e.g. descendants]] - [[#helm-command][Helm command]] - [[#quickly-change-sortinggrouping-in-search-views][Quickly change sorting/grouping in search views]] @@ -214,114 +215,6 @@ I should benchmark it to see how much difference it makes, because all those ~fs ** TODO [#C] Update commentary -** NEXT [#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. - -** UNDERWAY [#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. - -#+BEGIN_SRC elisp - (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)))) -#+END_SRC - ** 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. @@ -339,6 +232,10 @@ However, there might still be a useful idea here somewhere... 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. +** NEXT [#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. + ** PROJECT [#A] Compatibility with Org 9.4 custom link changes :bug: :PROPERTIES: :milestone: 0.5 @@ -362,6 +259,7 @@ Unfortunately it does not say what the new, required second argument is. ** PROJECT [#A] Fix =org-ql-view--link-open= on Org 9.3+ :compatibility:bug: :PROPERTIES: :milestone: 0.5 +:url: https://github.com/alphapapa/org-ql/issues/147 :END: 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. @@ -1487,6 +1385,110 @@ e.g. doesn't currently show the =View= header. 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. +** UNDERWAY [#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. + +#+BEGIN_SRC elisp + (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)))) +#+END_SRC + ** DONE [#A] Fix query-sexp-to-string function's handling of, e.g. =descendants= :bug: :PROPERTIES: :milestone: 0.5 From 8a1fd1459081f5639a8c2d58289bb1fe4e095ea9 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Sat, 14 Nov 2020 22:29:53 -0600 Subject: [PATCH 029/372] Fix: (org-ql-view--link-store) Indirect buffers Probably the best thing to do, even if it's not "correct" in a sense. --- org-ql-view.el | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/org-ql-view.el b/org-ql-view.el index 0101a69..062c9e9 100644 --- a/org-ql-view.el +++ b/org-ql-view.el @@ -662,10 +662,15 @@ When opened, the link searches the buffer it's opened from." ("containing file" nil) (buffers-files (prin1-to-string buffers-files)))) (string-or-file-buffer-p - (thing) (cl-typecase thing + (thing) (cl-etypecase thing (string thing) (buffer (or (buffer-file-name thing) - (buffer-file-name (buffer-base-buffer thing))))))) + ;; TODO: Should indirect buffers be allowed? Maybe not, since their narrowing isn't preserved. + ;; On the other hand, it's possible to accidentally make a search view for an indirect buffer + ;; that's since been widened, and forcing the user to manually change that would be awkward, + ;; and trying to communicate the problem would be difficult, so maybe it's okay to allow it. + (when (buffer-base-buffer thing) + (buffer-file-name (buffer-base-buffer thing)))))))) (unless (or (string-or-file-buffer-p org-ql-view-buffers-files) (and (listp org-ql-view-buffers-files) (cl-every #'string-or-file-buffer-p org-ql-view-buffers-files))) From b04d0263a4e0d072a8c7fbb6d50af24f5193b236 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Sun, 15 Nov 2020 21:07:51 -0600 Subject: [PATCH 030/372] Notes: Update --- notes.org | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/notes.org b/notes.org index ebfa4a9..c15d42a 100644 --- a/notes.org +++ b/notes.org @@ -82,6 +82,7 @@ :TOC: :include descendants :depth 1 :END: :CONTENTS: +- [[#add-emacs-271-to-testyml][Add Emacs 27.1 to test.yml]] - [[#add-auto-keyword-to-planning-predicate][Add :auto keyword to (planning) predicate]] - [[#change-deadlines-auto-argument-to-auto-andor-auto-t][Change (deadline)'s auto argument to :auto and/or :auto t]] - [[#outline-path-in-buffers-files-arg][Outline path in buffers-files arg]] @@ -122,6 +123,11 @@ - [[#use-macros-for-date][Use macros for date]] :END: +** TODO [#A] Add Emacs 27.1 to =test.yml= +:PROPERTIES: +:milestone: 0.5 +:END: + ** TODO [#B] Add ~:auto~ keyword to ~(planning)~ predicate It should act like ~(or (deadline auto) (scheduled :to today))~. From 9d86ee9e1098fef5b4a17e98321f522f6d8fc20b Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Mon, 16 Nov 2020 02:02:22 -0600 Subject: [PATCH 031/372] Change/Fix: Bookmark, link storing/loading/safety This makes several changes and fixes and adds test cases for all the combinations I'm aware of. See #147. Thanks to @tpeacock19 for patiently reporting these issues. Squashed commit of the following: commit d1b396c5654ae3fa8ad03d0d4b2cbde20000fa1a Author: Adam Porter Date: Mon Nov 16 02:01:23 2020 -0600 WIP: Fixes commit 23215a723c4bf6e249c330863e01029166db56c3 Author: Adam Porter Date: Mon Nov 16 01:34:34 2020 -0600 WIP: Fixes commit 97b13d09d4d52bb168bfc19fb6f8daf2964f4b6c Author: Adam Porter Date: Mon Nov 16 00:46:49 2020 -0600 WIP: Test links too Everything passes now. commit 1866a71dbbb6416f7e365c90cadd269765d52b70 Author: Adam Porter Date: Sun Nov 15 22:22:22 2020 -0600 WIP: Test loading/saving bookmarks --- org-ql-view.el | 62 +++++---- org-ql.el | 2 + tests/test-org-ql.el | 321 ++++++++++++++++++++++++++++++++++++------- 3 files changed, 310 insertions(+), 75 deletions(-) diff --git a/org-ql-view.el b/org-ql-view.el index 062c9e9..8e8d40b 100644 --- a/org-ql-view.el +++ b/org-ql-view.el @@ -620,7 +620,7 @@ protocol. See, e.g. `org-ql-view--link-store'." (read-value (read stored-string))) ;; Ensure the value is either a symbol or list of symbols (which excludes lambdas). (unless (or (symbolp read-value) (cl-every #'symbolp read-value)) - (error "Potentially unsafe value found in link's SORT parameter (%s). Link not opened" + (error "CAUTION: Link not opened because unsafe sort parameter detected: %s" read-value)) read-value)) (org-super-agenda-allow-unsafe-groups nil) ; Disallow unsafe group selectors. @@ -631,6 +631,10 @@ protocol. See, e.g. `org-ql-view--link-store'." (buffers-files (--if-let (alist-get "buffers-files" params nil nil #'string=) (org-ql-view--expand-buffers-files (read it)) (current-buffer)))) + (unless (or (bufferp buffers-files) + (stringp buffers-files) + (cl-every #'stringp buffers-files)) + (error "CAUTION: Link not opened because unsafe buffers-files parameter detected: %s" buffers-files)) (when (and org-ql-view-ask-unsafe-links (or (string-match (rx bol (0+ space) "(") query) (listp query))) @@ -655,31 +659,31 @@ When opened, the link searches the buffer it's opened from." (require 'url-util) (when org-ql-view-query ;; Only Org QL View buffers should have `org-ql-view-query' set. - (cl-flet ((prompt-for (buffers-files) - (pcase-exhaustive - (completing-read (format "Link to search file containing inserted link or %s? " buffers-files) - (list "containing file" buffers-files) nil t) - ("containing file" nil) - (buffers-files (prin1-to-string buffers-files)))) - (string-or-file-buffer-p - (thing) (cl-etypecase thing - (string thing) - (buffer (or (buffer-file-name thing) - ;; TODO: Should indirect buffers be allowed? Maybe not, since their narrowing isn't preserved. - ;; On the other hand, it's possible to accidentally make a search view for an indirect buffer - ;; that's since been widened, and forcing the user to manually change that would be awkward, - ;; and trying to communicate the problem would be difficult, so maybe it's okay to allow it. - (when (buffer-base-buffer thing) - (buffer-file-name (buffer-base-buffer thing)))))))) - (unless (or (string-or-file-buffer-p org-ql-view-buffers-files) - (and (listp org-ql-view-buffers-files) - (cl-every #'string-or-file-buffer-p org-ql-view-buffers-files))) + (cl-labels ((prompt-for (buffers-files) + (pcase-exhaustive + (completing-read (format "Make link that searches: ") + '("file link is in" "files currently searched") + nil t nil nil "file link is in") + ("file link is in" nil) + ("files currently searched" buffers-files))) + (strings-or-file-buffers-p + (thing) (cl-etypecase thing + (list (cl-every #'strings-or-file-buffers-p thing)) + (string thing) + (buffer (or (buffer-file-name thing) + ;; TODO: Should indirect buffers be allowed? Maybe not, since their narrowing isn't preserved. + ;; On the other hand, it's possible to accidentally make a search view for an indirect buffer + ;; that's since been widened, and forcing the user to manually change that would be awkward, + ;; and trying to communicate the problem would be difficult, so maybe it's okay to allow it. + (when (buffer-base-buffer thing) + (buffer-file-name (buffer-base-buffer thing)))))))) + (unless (strings-or-file-buffers-p org-ql-view-buffers-files) (user-error "Views that search non-file-backed buffers can't be linked to")) (let* ((query-string (--if-let (org-ql--query-sexp-to-string org-ql-view-query) it (org-ql-view--format-query org-ql-view-query))) (buffers-files (prompt-for (org-ql-view--contract-buffers-files org-ql-view-buffers-files))) (params (list (when buffers-files - (list "buffers-files" buffers-files)) + (list "buffers-files" (prin1-to-string buffers-files))) (when org-ql-view-super-groups (list "super-groups" (prin1-to-string org-ql-view-super-groups))) (when org-ql-view-sort @@ -1037,16 +1041,14 @@ current buffer. Otherwise BUFFERS-FILES is returned unchanged." "org-agenda-files") ((pred (seq-set-equal-p (org-ql-search-directories-files))) "org-directory") - (_ (let ((print-length nil)) - (concat "'" (prin1-to-string buffers-files)))))) + (_ buffers-files))) ((pred (equal (current-buffer))) "buffer") ((or 'org-agenda-files '(function org-agenda-files)) "org-agenda-files") ((and (pred bufferp) (guard (buffer-file-name buffers-files))) (buffer-file-name buffers-files)) - (_ (let ((print-length nil)) - (prin1-to-string buffers-files)))))) + (_ buffers-files)))) (defun org-ql-view--complete-buffers-files () "Return value for `org-ql-view-buffers-files' using completion." @@ -1068,13 +1070,15 @@ current buffer. Otherwise BUFFERS-FILES is returned unchanged." "Return BUFFERS-FILES expanded to a list of files or buffers. The counterpart to `org-ql-view--contract-buffers-files'." (pcase-exhaustive buffers-files - ((pred bufferp) buffers-files) - ((or "" "buffer") (current-buffer)) - ("org-agenda-files" (org-agenda-files)) ("all" (--select (equal (buffer-local-value 'major-mode it) 'org-mode) (buffer-list))) + ("org-agenda-files" (org-agenda-files)) ("org-directory" (org-ql-search-directories-files)) - (else (s-split (rx (1+ space)) else)))) + ((or "" "buffer") (current-buffer)) + ((pred bufferp) buffers-files) + ((pred listp) buffers-files) + ;; A single filename. + ((pred stringp) buffers-files))) (defun org-ql-view--complete-super-groups () "Return value for `org-ql-view-super-groups' using completion." diff --git a/org-ql.el b/org-ql.el index 3825ec2..3b97307 100644 --- a/org-ql.el +++ b/org-ql.el @@ -225,6 +225,8 @@ returns nil or non-nil." (list buffers-or-files) (otherwise (list buffers-or-files))) (--map (cl-etypecase it + ;; NOTE: This etypecase is essential to opening links safely, + ;; as it rejects, e.g. lambdas in the buffers-files argument. (buffer it) (string (or (find-buffer-visiting it) (when (file-readable-p it) diff --git a/tests/test-org-ql.el b/tests/test-org-ql.el index 95519e8..f9499b7 100644 --- a/tests/test-org-ql.el +++ b/tests/test-org-ql.el @@ -3,7 +3,7 @@ ;; Copyright (C) 2019 Adam Porter ;; Author: Adam Porter -;; Package-Requires: ((buttercup)) +;; Package-Requires: ((buttercup) (with-simulated-input)) ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by @@ -27,6 +27,7 @@ ;;;; Requirements (require 'buttercup) +(require 'with-simulated-input) (require 'org-ql) @@ -1166,6 +1167,11 @@ RESULTS should be a list of strings as returned by ;; different reason. Were it not for testing the specific error AND its arguments, that test ;; case would have apparently passed, but it would have hidden the mistake in the test. + ;; NOTE: Rather than calling `message' in the "evil" lambdas (which, if it succeeds + ;; in being evil, merely writes a string to the test output, which is EASILY missed), + ;; we call `error', which, in combination with testing for specific error types and + ;; arguments, correctly causes tests to fail if the unsafe condition is not caught. + ;; Also, while writing these tests, the version of org-super-agenda in the test sandbox does ;; not have the fix applied yet, and the test for that apparently, correctly does not pass yet. @@ -1187,72 +1193,295 @@ RESULTS should be a list of strings as returned by (backward-char 1) (call-interactively #'org-open-at-point)))) - (it "buffers-files parameter" - (let ((quoted-lambda-link "[[org-ql-search:todo:?buffers-files%3D%28lambda%20nil%20%28message%20%22AHA%22%29%29]]") - (unquoted-lambda-link "[[org-ql-search:todo:?buffers-files%3D%28lambda%20nil%20%28message%20%22AHA%22%29%29]]") - (quoted-lambda-in-list-link "[[org-ql-search:todo:?buffers-files%3D%28%28quote%20%28lambda%20nil%20%28message%20%22AHA%22%29%29%29%29]]") - (unquoted-lambda-in-list-link "[[org-ql-search:todo:?buffers-files%3D%28%28lambda%20nil%20%28message%20%22AHA%22%29%29%29]]")) + (describe "buffers-files parameter" + :var ((quoted-lambda-link "[[org-ql-search:todo:?buffers-files%3D%28lambda%20nil%20%28error%20%22UNSAFE%22%29%29]]") + (unquoted-lambda-link "[[org-ql-search:todo:?buffers-files%3D%28lambda%20nil%20%28error%20%22UNSAFE%22%29%29]]") + (quoted-lambda-in-list-link "[[org-ql-search:todo:?buffers-files%3D%28%28quote%20%28lambda%20nil%20%28error%20%22UNSAFE%22%29%29%29%29]]") + (unquoted-lambda-in-list-link "[[org-ql-search:todo:?buffers-files%3D%28%28lambda%20nil%20%28error%20%22UNSAFE%22%29%29%29]]")) + (it "Errors for a quoted lambda" (expect (open-link quoted-lambda-link) - :to-throw 'wrong-type-argument '(stringp (lambda nil (message "AHA")))) + :to-throw 'error '("CAUTION: Link not opened because unsafe buffers-files parameter detected: (lambda nil (error UNSAFE))"))) + (it "Errors for an unquoted lambda" (expect (open-link unquoted-lambda-link) - :to-throw 'wrong-type-argument '(stringp (lambda nil (message "AHA")))) + :to-throw 'error '("CAUTION: Link not opened because unsafe buffers-files parameter detected: (lambda nil (error UNSAFE))"))) + (it "Errors for a quoted lambda in a list" (expect (open-link quoted-lambda-in-list-link) - :to-throw 'wrong-type-argument '(stringp ((quote (lambda nil (message "AHA")))))) + :to-throw 'error '("CAUTION: Link not opened because unsafe buffers-files parameter detected: ((quote (lambda nil (error UNSAFE))))"))) + (it "Errors for an unquoted lambda in a list" (expect (open-link unquoted-lambda-in-list-link) - :to-throw 'wrong-type-argument '(stringp ((lambda nil (message "AHA"))))))) + :to-throw 'error '("CAUTION: Link not opened because unsafe buffers-files parameter detected: ((lambda nil (error UNSAFE)))")))) - (it "super-groups parameter" - (let ((quoted-lambda-link "[[org-ql-search:todo:?super-groups%3D%28lambda%20nil%20%28message%20%22AHA%22%29%29]]") - (unquoted-lambda-link "[[org-ql-search:todo:?super-groups%3D%28lambda%20nil%20%28message%20%22AHA%22%29%29]]") - (quoted-expression-link "[[org-ql-search:todo:?super-groups%3D%28message%20%22AHA%22%29]]") - (unquoted-expression-link "[[org-ql-search:todo:?super-groups%3D%22AHA%22]]") - (pred-selector-link "[[org-ql-search:todo:?super-groups%3D%28%28%3Apred%20%28lambda%20%28_%29%20%28message%20%22AHA%22%29%29%29%29]]") - (auto-map-selector-link "[[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]]")) + (describe "super-groups parameter" + :var ((quoted-lambda-link "[[org-ql-search:todo:?super-groups%3D%28lambda%20nil%20%28error%20%22UNSAFE%22%29%29]]") + (unquoted-lambda-link "[[org-ql-search:todo:?super-groups%3D%28lambda%20nil%20%28error%20%22UNSAFE%22%29%29]]") + (quoted-expression-link "[[org-ql-search:todo:?super-groups%3D%28error%20%22UNSAFE%22%29]]") + (unquoted-expression-link "[[org-ql-search:todo:?super-groups%3D%22UNSAFE%22]]") + (pred-selector-link "[[org-ql-search:todo:?super-groups%3D%28%28%3Apred%20%28lambda%20%28_%29%20%28error%20%22UNSAFE%22%29%29%29%29]]") + (auto-map-selector-link "[[org-ql-search:todo:?super-groups%3D%28%28%3Aauto-map%20%28lambda%20%28_%29%20%28error%20%22UNSAFE%22%29%29%29%29]]")) + (it "Errors for a quoted lambda" (expect (open-link quoted-lambda-link) - :to-throw 'wrong-type-argument '(listp lambda)) + :to-throw 'wrong-type-argument '(listp lambda))) + (it "Errors for an unquoted lambda" (expect (open-link unquoted-lambda-link) - :to-throw 'wrong-type-argument '(listp lambda)) + :to-throw 'wrong-type-argument '(listp lambda))) + (it "Errors for a quoted expression" (expect (open-link quoted-expression-link) - :to-throw 'wrong-type-argument '(listp message)) + :to-throw 'wrong-type-argument '(listp error))) + (it "Errors for an unquoted expression" (expect (open-link unquoted-expression-link) - :to-throw 'error '("cl-etypecase failed: AHA, (symbol list)")) + :to-throw 'error '("cl-etypecase failed: UNSAFE, (symbol list)"))) - ;; FIXME: These two tests will not pass (i.e. they will not - ;; signal an error) until the version of org-super-agenda in - ;; the test sandbox is upgraded, which I'll do when MELPA - ;; packages the latest version. On the bright side, I just - ;; confirmed that, without the fix to org-super-agenda, - ;; these are vulnerable, which means that the test catches - ;; this problem and detects the fix. + ;; NOTE: These two tests depend on `org-super-agenda' to signal these errors. It's probably better + ;; to catch these in `org-super-agenda' rather than in `org-ql', because if other potentially unsafe + ;; selectors were added to org-super-agenda, org-ql would have to play catch-up, adding more tests. + ;; Catching them in org-super-agenda means that it can add more checks itself in the future. + (it "Errors for a :pred group" (expect (open-link pred-selector-link) - :to-throw 'error '("Unsafe groups disallowed (:pred): (lambda (_) (message AHA))")) + :to-throw 'error '("Unsafe groups disallowed (:pred): (lambda (_) (error UNSAFE))"))) + (it "Errors for an :auto-map group" (expect (open-link auto-map-selector-link) - :to-throw 'error '("Unsafe groups disallowed (:auto-map): ((lambda (_) (message AHA)))")))) + :to-throw 'error '("Unsafe groups disallowed (:auto-map): ((lambda (_) (error UNSAFE)))")))) - (it "title parameter" - (let ((quoted-lambda-link "[[org-ql-search:todo:?title%3D%28lambda%20%28_%20_%29%20%28message%20%22AHA%22%29%29]]") - (unquoted-lambda-link "[[org-ql-search:todo:?title%3D%28lambda%20%28_%20_%29%20%28message%20%22AHA%22%29%29]]") - (expression-link "[[org-ql-search:todo:?title%3D%28message%20%22AHA%22%29]]")) + (describe "title parameter" + :var ((quoted-lambda-link "[[org-ql-search:todo:?title%3D%28lambda%20%28_%20_%29%20%28error%20%22UNSAFE%22%29%29]]") + (unquoted-lambda-link "[[org-ql-search:todo:?title%3D%28lambda%20%28_%20_%29%20%28error%20%22UNSAFE%22%29%29]]") + (expression-link "[[org-ql-search:todo:?title%3D%28error%20%22UNSAFE%22%29]]")) + (it "Errors for a quoted lambda" (expect (open-link quoted-lambda-link) - :to-throw 'wrong-type-argument '(characterp lambda)) + :to-throw 'wrong-type-argument '(characterp lambda))) + (it "Errors for an unquoted lambda" (expect (open-link unquoted-lambda-link) - :to-throw 'wrong-type-argument '(characterp lambda)) + :to-throw 'wrong-type-argument '(characterp lambda))) + (it "Errors for an expression" (expect (open-link expression-link) - :to-throw 'wrong-type-argument '(characterp message)))) + :to-throw 'wrong-type-argument '(characterp error)))) - (it "sort parameter" - (let ((quoted-lambda-link "[[org-ql-search:todo:?sort%3D%28lambda%20%28_%20_%29%20%28message%20%22AHA%22%29%29]]") - (unquoted-lambda-link "[[org-ql-search:todo:?sort%3D%28lambda%20%28_%20_%29%20%28message%20%22AHA%22%29%29]]") - (quoted-lambda-in-list-link "[[org-ql-search:todo:?sort%3D%28%28quote%20%28lambda%20%28_%20_%29%20%28message%20%22AHA%22%29%29%29%29]]") - (unquoted-lambda-in-list-link "[[org-ql-search:todo:?sort=((lambda%20nil%20(message%20\"AHA\")))]]")) + (describe "sort parameter" + :var ((quoted-lambda-link "[[org-ql-search:todo:?sort%3D%28lambda%20%28_%20_%29%20%28error%20%22UNSAFE%22%29%29]]") + (unquoted-lambda-link "[[org-ql-search:todo:?sort%3D%28lambda%20%28_%20_%29%20%28error%20%22UNSAFE%22%29%29]]") + (quoted-lambda-in-list-link "[[org-ql-search:todo:?sort%3D%28%28quote%20%28lambda%20%28_%20_%29%20%28error%20%22UNSAFE%22%29%29%29%29]]") + (unquoted-lambda-in-list-link "[[org-ql-search:todo:?sort=((lambda%20nil%20(error%20\"UNSAFE\")))]]")) + (it "Errors for a quoted lambda" (expect (open-link quoted-lambda-link) - :to-throw 'error '("Potentially unsafe value found in link’s SORT parameter ((lambda (_ _) (message AHA))). Link not opened")) + :to-throw 'error '("CAUTION: Link not opened because unsafe sort parameter detected: (lambda (_ _) (error UNSAFE))"))) + (it "Errors for an unquoted lambda" (expect (open-link unquoted-lambda-link) - :to-throw 'error '("Potentially unsafe value found in link’s SORT parameter ((lambda (_ _) (message AHA))). Link not opened")) + :to-throw 'error '("CAUTION: Link not opened because unsafe sort parameter detected: (lambda (_ _) (error UNSAFE))"))) + (it "Errors for a quoted lambda in a list" (expect (open-link quoted-lambda-in-list-link) - :to-throw 'error '("Potentially unsafe value found in link’s SORT parameter (((quote (lambda (_ _) (message AHA))))). Link not opened")) + :to-throw 'error '("CAUTION: Link not opened because unsafe sort parameter detected: ((quote (lambda (_ _) (error UNSAFE))))"))) + (it "Errors for an unquoted lambda in a list" (expect (open-link unquoted-lambda-in-list-link) - :to-throw 'error '("Potentially unsafe value found in link’s SORT parameter (((lambda nil (message AHA)))). Link not opened")))))))) + :to-throw 'error '("CAUTION: Link not opened because unsafe sort parameter detected: ((lambda nil (error UNSAFE)))"))))))) + + (describe "View saving/loading" + :var* ((temp-dir (make-temp-file "test-org-ql-" 'dir)) + (temp-filenames (cl-loop for file in '("test1.org" "test2.org") + collect (expand-file-name file temp-dir))) + (file-contents (with-temp-buffer + (insert "#+TITLE: Test data\n\n" + "* TODO Heading 1\n" + "Heading 1 text.\n\n" + "* Heading 2\n" + "Heading 2 text.\n") + (buffer-string)))) + + ;; This section will test saving and loading search views by a few different + ;; means. In each one, the values of these buffer-local variables will be + ;; stored before saving a view and compared after loading it: + + ;; - org-ql-view-buffers-files + ;; - org-ql-view-query + ;; - org-ql-view-sort + ;; - org-ql-view-super-groups + ;; - org-ql-view-title + + ;; `org-ql-view-buffers-files' needs to be tested with these kinds of values: + + ;; - A buffer + ;; - A string (filename) + ;; - List of buffers + ;; - List of strings (filenames) + ;; - Combination + + ;; `org-ql-view-query' needs to be tested with these kinds of values: + + ;; - String query + ;; - Sexp query + + ;; `org-ql-view-sort' needs to be tested with these kinds of values: + + ;; - Symbol + ;; - List of symbols + + ;; `org-ql-view-super-groups' can probably be tested with nearly + ;; any list value (because this set of tests is not intended to + ;; test link safety). + + ;; TODO: `org-ql-view-title' needs to be tested with a string value. + + (before-all + (dolist (filename temp-filenames) + (with-temp-file filename + (insert file-contents)))) + + (after-all + (delete-directory temp-dir 'recursive)) + + (describe "Bookmarks" + :var ((title "ORG-QL-TEST") + bookmark-alist + view-buffer) + + (before-each + (setf view-buffer (get-buffer-create "*TEST*")) + (dolist (filename temp-filenames) + ;; Kill any existing buffers visiting these files. + (when-let ((buffer (find-file-noselect filename 'nowarn))) + (kill-buffer buffer)))) + + (cl-flet ((var-after-bookmark-set-and-jump + (var buffers-files query &key sort super-groups) + (org-ql-search buffers-files query + :super-groups super-groups + :sort sort :title title :buffer view-buffer) + (set-buffer view-buffer) + (bookmark-set title) + (kill-buffer) + (bookmark-jump title) + (buffer-local-value var (get-buffer (concat "*Org QL View: " title "*"))))) + + (describe "Grouping" + :var ((query '(and (todo "TODO") (regexp "heading"))) + (super-groups '((:auto-priority)))) + (it "is restored" + (expect (var-after-bookmark-set-and-jump 'org-ql-view-super-groups temp-filenames query + :super-groups super-groups) + :to-equal super-groups))) + + (describe "Queries" + :var ((string-query "todo:TODO regexp:heading") + (sexp-query '(and (todo "TODO") (regexp "heading")))) + (it "Sexps match" + (expect (var-after-bookmark-set-and-jump 'org-ql-view-query temp-filenames sexp-query) :to-equal sexp-query)) + (it "Strings match" + ;; NOTE: This test includes the string query being replaced with its sexp form after the query is run. + (expect (var-after-bookmark-set-and-jump 'org-ql-view-query temp-filenames string-query) + :to-equal sexp-query))) + + (describe "Sorting" + :var ((query '(and (todo "TODO") (regexp "heading"))) + (sorter 'todo) + (multiple-sorters '(todo priority))) + (it "One sorter is restored" + (expect (var-after-bookmark-set-and-jump 'org-ql-view-sort temp-filenames query :sort sorter) + :to-equal sorter)) + (it "Multiple sorters are restored" + ;; NOTE: This test includes the string query being replaced with its sexp form after the query is run. + (expect (var-after-bookmark-set-and-jump 'org-ql-view-sort temp-filenames query :sort multiple-sorters) + :to-equal multiple-sorters))) + + (describe "Buffers/Files" + :var ((query '(and (todo "TODO") (regexp "heading"))) + (one-filename (car temp-filenames))) + (it "One filename matches" + (expect (var-after-bookmark-set-and-jump 'org-ql-view-buffers-files one-filename query) + :to-equal one-filename)) + (it "A list of filenames matches" + (expect (var-after-bookmark-set-and-jump 'org-ql-view-buffers-files temp-filenames query) + :to-equal temp-filenames)) + ;; NOTE: These actually bookmark the filenames backing the buffers. + (it "One buffer matches" + (expect (var-after-bookmark-set-and-jump 'org-ql-view-buffers-files + (find-file-noselect (car temp-filenames)) query) + :to-equal one-filename)) + (it "A list of buffers matches" + (expect (var-after-bookmark-set-and-jump 'org-ql-view-buffers-files + (mapcar #'find-file-noselect temp-filenames) query) + :to-equal temp-filenames))))) + + (describe "Links" + ;; Not sure if this binding works. + :var ((title "ORG-QL-TEST") + (view-buffer-name "*TEST VIEW BUFFER*") + link-buffer view-buffer) + + (before-each + (kill-buffer (get-buffer view-buffer-name)) + (setf link-buffer (get-buffer-create "*TEST LINK BUFFER*") + view-buffer (get-buffer-create view-buffer-name)) + (with-current-buffer link-buffer + (erase-buffer) + (insert "* TODO Test heading\n\n") + (org-mode))) + + (cl-flet ((var-after-link-save-open + (var buffers-files query &key sort super-groups (input "RET")) + (org-ql-search buffers-files query + :super-groups super-groups + :sort sort :title title :buffer view-buffer) + (with-current-buffer view-buffer + (with-simulated-input input + ;; Avoid writing "Stored: ..." to test output. + (let ((inhibit-message t)) + (org-store-link nil))) + (kill-buffer)) + ;; ;; Let's see if this trick works. + (org-open-link-from-string (caar org-stored-links) nil link-buffer) + (with-current-buffer (get-buffer (concat "*Org QL View: " title "*")) + (prog1 (buffer-local-value var (current-buffer)) + (kill-buffer))))) + + (describe "Queries" + :var ((string-query "todo:TODO regexp:heading") + (sexp-query '(and (todo "TODO") (regexp "heading")))) + (it "Sexps match" + (expect (var-after-link-save-open 'org-ql-view-query temp-filenames sexp-query) :to-equal sexp-query)) + (it "Strings match" + ;; NOTE: This test includes the string query being replaced with its sexp form after the query is run. + (expect (var-after-link-save-open 'org-ql-view-query temp-filenames string-query) + :to-equal sexp-query))) + + (describe "Grouping" + :var ((query '(and (todo "TODO") (regexp "heading"))) + (super-groups '((:auto-priority)))) + (it "is restored" + (expect (var-after-link-save-open 'org-ql-view-super-groups temp-filenames query + :super-groups super-groups) + :to-equal super-groups))) + + (describe "Sorting" + :var ((query '(and (todo "TODO") (regexp "heading"))) + (sorter 'todo) + (multiple-sorters '(todo priority))) + (it "One sorter is restored" + (expect (var-after-link-save-open 'org-ql-view-sort temp-filenames query :sort sorter) + :to-equal sorter)) + (it "Multiple sorters are restored" + ;; NOTE: This test includes the string query being replaced with its sexp form after the query is run. + (expect (var-after-link-save-open 'org-ql-view-sort temp-filenames query :sort multiple-sorters) + :to-equal multiple-sorters))) + + (describe "Buffers/Files" + :var ((query '(and (todo "TODO") (regexp "heading"))) + (one-buffer (find-file-noselect (car temp-filenames))) + (one-filename (car temp-filenames)) + (buffers (mapcar #'find-file-noselect temp-filenames))) + (it "Can search buffer containing the link" + (expect (var-after-link-save-open 'org-ql-view-buffers-files one-filename query) + :to-equal link-buffer)) + (it "Can search a file by filename" + (expect (var-after-link-save-open 'org-ql-view-buffers-files one-filename query + :input "M-n M-n RET") + :to-equal one-filename)) + (it "Can search multiple files by filename" + (expect (var-after-link-save-open 'org-ql-view-buffers-files temp-filenames query + :input "M-n M-n RET") + :to-equal temp-filenames))))) + + ;; MAYBE: Also test `org-ql-views', although I already know it works now. + ;; (describe "org-ql-views") + )) ;; Local Variables: ;; truncate-lines: t From e39fb1be5a16202f6f757f170781aebced955ae5 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Mon, 16 Nov 2020 02:11:38 -0600 Subject: [PATCH 032/372] Tests: Tidy --- tests/test-org-ql.el | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test-org-ql.el b/tests/test-org-ql.el index f9499b7..ee9b772 100644 --- a/tests/test-org-ql.el +++ b/tests/test-org-ql.el @@ -1464,9 +1464,7 @@ RESULTS should be a list of strings as returned by (describe "Buffers/Files" :var ((query '(and (todo "TODO") (regexp "heading"))) - (one-buffer (find-file-noselect (car temp-filenames))) - (one-filename (car temp-filenames)) - (buffers (mapcar #'find-file-noselect temp-filenames))) + (one-filename (car temp-filenames))) (it "Can search buffer containing the link" (expect (var-after-link-save-open 'org-ql-view-buffers-files one-filename query) :to-equal link-buffer)) From 0d12cdb3f5a0bc2a80803ae762c26766c76ceafe Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Mon, 16 Nov 2020 02:25:45 -0600 Subject: [PATCH 033/372] Fix: (org-ql-search-directories-files) Error unless org-directory Fixes #139. Thanks to Matt Huszagh (@matthuszagh) for reporting. Released as 0.4.7. --- README.org | 5 ++ org-ql-search.el | 9 ++-- org-ql.info | 130 ++++++++++++++++++++++++++--------------------- 3 files changed, 84 insertions(+), 60 deletions(-) diff --git a/README.org b/README.org index a7d79eb..23c4a9d 100644 --- a/README.org +++ b/README.org @@ -398,6 +398,11 @@ Expands into a call to ~org-ql-select~ with the same arguments. For convenience /Note:/ Breaking changes may be made before version 1.0, but in the event of major changes, attempts at backward compatibility will be made with obsolescence declarations, translation of arguments, etc. Users who need stability guarantees before 1.0 may choose to use tagged stable releases. +** 0.4.7 + +*Fixed* ++ Give a useful error if =org-ql-search-directories-files= is called without a directories argument and =org-directory= doesn't exist. (Fixes [[https://github.com/alphapapa/org-ql/issues/139][#139]]. Thanks to [[https://github.com/matthuszagh][Matt Huszagh]] for reporting.) + ** 0.4.6 *Fixed* diff --git a/org-ql-search.el b/org-ql-search.el index d97d59e..cf49281 100644 --- a/org-ql-search.el +++ b/org-ql-search.el @@ -270,9 +270,12 @@ automatically from the query." ;;;; Functions -(cl-defun org-ql-search-directories-files (&key (directories (list org-directory)) - (recurse org-ql-search-directories-files-recursive) - (regexp org-ql-search-directories-files-regexp)) +(cl-defun org-ql-search-directories-files + (&key (directories (if (file-exists-p org-directory) + (list org-directory) + (user-error "org-ql-search-directories-files: No DIRECTORIES given, and `org-directory' doesn't exist"))) + (recurse org-ql-search-directories-files-recursive) + (regexp org-ql-search-directories-files-regexp)) "Return list of matching files in DIRECTORIES, a list of directory paths. When RECURSE is non-nil, recurse into subdirectories. When REGEXP is non-nil, only return files that match REGEXP." diff --git a/org-ql.info b/org-ql.info index 213b610..6e8a77d 100644 --- a/org-ql.info +++ b/org-ql.info @@ -63,6 +63,7 @@ Functions / Macros Changelog +* 0.4.7: 047. * 0.4.6: 046. * 0.4.5: 045. * 0.4.4: 044. @@ -709,6 +710,7 @@ releases. * Menu: +* 0.4.7: 047. * 0.4.6: 046. * 0.4.5: 045. * 0.4.4: 044. @@ -726,9 +728,22 @@ releases. * 0.1: 01.  -File: README.info, Node: 046, Next: 045, Up: Changelog +File: README.info, Node: 047, Next: 046, Up: Changelog -5.1 0.4.6 +5.1 0.4.7 +========= + +*Fixed* + • Give a useful error if org-ql-search-directories-files is called + without a directories argument and org-directory doesn’t exist. + (Fixes #139 (https://github.com/alphapapa/org-ql/issues/139). + Thanks to Matt Huszagh (https://github.com/matthuszagh) for + reporting.) + + +File: README.info, Node: 046, Next: 045, Prev: 047, Up: Changelog + +5.2 0.4.6 ========= *Fixed* @@ -741,7 +756,7 @@ File: README.info, Node: 046, Next: 045, Up: Changelog  File: README.info, Node: 045, Next: 044, Prev: 046, Up: Changelog -5.2 0.4.5 +5.3 0.4.5 ========= *Fixed* @@ -753,7 +768,7 @@ File: README.info, Node: 045, Next: 044, Prev: 046, Up: Changelog  File: README.info, Node: 044, Next: 043, Prev: 045, Up: Changelog -5.3 0.4.4 +5.4 0.4.4 ========= *Fixed* @@ -765,7 +780,7 @@ File: README.info, Node: 044, Next: 043, Prev: 045, Up: Changelog  File: README.info, Node: 043, Next: 042, Prev: 044, Up: Changelog -5.4 0.4.3 +5.5 0.4.3 ========= *Fixed* @@ -775,7 +790,7 @@ File: README.info, Node: 043, Next: 042, Prev: 044, Up: Changelog  File: README.info, Node: 042, Next: 041, Prev: 043, Up: Changelog -5.5 0.4.2 +5.6 0.4.2 ========= *Fixed* @@ -784,7 +799,7 @@ File: README.info, Node: 042, Next: 041, Prev: 043, Up: Changelog  File: README.info, Node: 041, Next: 04, Prev: 042, Up: Changelog -5.6 0.4.1 +5.7 0.4.1 ========= *Fixed* @@ -794,7 +809,7 @@ File: README.info, Node: 041, Next: 04, Prev: 042, Up: Changelog  File: README.info, Node: 04, Next: 032, Prev: 041, Up: Changelog -5.7 0.4 +5.8 0.4 ======= _Note:_ The next release, 0.5, may include changes which will require @@ -875,7 +890,7 @@ as they will be pushed to the master branch when ready.  File: README.info, Node: 032, Next: 031, Prev: 04, Up: Changelog -5.8 0.3.2 +5.9 0.3.2 ========= *Fixed* @@ -888,8 +903,8 @@ File: README.info, Node: 032, Next: 031, Prev: 04, Up: Changelog  File: README.info, Node: 031, Next: 03, Prev: 032, Up: Changelog -5.9 0.3.1 -========= +5.10 0.3.1 +========== *Fixed* • Compatibility with Org 9.2. Thanks to Brian Leung @@ -898,7 +913,7 @@ File: README.info, Node: 031, Next: 03, Prev: 032, Up: Changelog  File: README.info, Node: 03, Next: 023, Prev: 031, Up: Changelog -5.10 0.3 +5.11 0.3 ======== *Added* @@ -963,7 +978,7 @@ File: README.info, Node: 03, Next: 023, Prev: 031, Up: Changelog  File: README.info, Node: 023, Next: 022, Prev: 03, Up: Changelog -5.11 0.2.3 +5.12 0.2.3 ========== *Fixed* @@ -973,7 +988,7 @@ File: README.info, Node: 023, Next: 022, Prev: 03, Up: Changelog  File: README.info, Node: 022, Next: 021, Prev: 023, Up: Changelog -5.12 0.2.2 +5.13 0.2.2 ========== *Fixed* @@ -984,7 +999,7 @@ File: README.info, Node: 022, Next: 021, Prev: 023, Up: Changelog  File: README.info, Node: 021, Next: 02, Prev: 022, Up: Changelog -5.13 0.2.1 +5.14 0.2.1 ========== *Fixed* @@ -994,7 +1009,7 @@ File: README.info, Node: 021, Next: 02, Prev: 022, Up: Changelog  File: README.info, Node: 02, Next: 01, Prev: 021, Up: Changelog -5.14 0.2 +5.15 0.2 ======== *Added* @@ -1077,7 +1092,7 @@ File: README.info, Node: 02, Next: 01, Prev: 021, Up: Changelog  File: README.info, Node: 01, Prev: 02, Up: Changelog -5.15 0.1 +5.16 0.1 ======== First tagged release. @@ -1138,46 +1153,47 @@ GPLv3  Tag Table: Node: Top225 -Node: Contents1492 -Node: Screenshots1666 -Node: Installation1784 -Node: Quelpa2422 -Node: Usage2865 -Node: Commands3214 -Node: org-ql-search3687 -Node: helm-org-ql5335 -Node: org-ql-view5747 -Node: org-ql-view-sidebar5945 -Node: org-ql-view-recent-items6301 -Node: org-ql-sparse-tree6785 -Node: Queries7585 -Node: Non-sexp query syntax8493 -Node: General predicates10200 -Node: Ancestor/descendant predicates15007 -Node: Date/time predicates16135 -Node: Functions / Macros18790 -Node: Agenda-like views19023 -Node: Listing / acting-on results20428 -Node: Changelog25030 -Node: 04625649 -Node: 04526037 -Node: 04426396 -Node: 04326753 -Node: 04226948 -Node: 04127107 -Node: 0427346 -Node: 03231277 -Node: 03131654 -Node: 0331849 -Node: 02334824 -Node: 02235052 -Node: 02135320 -Node: 0235519 -Node: 0139554 -Node: Notes39655 -Node: Comparison with Org Agenda searches39817 -Node: org-sidebar40688 -Node: License40967 +Node: Contents1507 +Node: Screenshots1681 +Node: Installation1799 +Node: Quelpa2437 +Node: Usage2880 +Node: Commands3229 +Node: org-ql-search3702 +Node: helm-org-ql5350 +Node: org-ql-view5762 +Node: org-ql-view-sidebar5960 +Node: org-ql-view-recent-items6316 +Node: org-ql-sparse-tree6800 +Node: Queries7600 +Node: Non-sexp query syntax8508 +Node: General predicates10215 +Node: Ancestor/descendant predicates15022 +Node: Date/time predicates16150 +Node: Functions / Macros18805 +Node: Agenda-like views19038 +Node: Listing / acting-on results20443 +Node: Changelog25045 +Node: 04725679 +Node: 04626062 +Node: 04526462 +Node: 04426821 +Node: 04327178 +Node: 04227373 +Node: 04127532 +Node: 0427771 +Node: 03231702 +Node: 03132079 +Node: 0332276 +Node: 02335251 +Node: 02235479 +Node: 02135747 +Node: 0235946 +Node: 0139981 +Node: Notes40082 +Node: Comparison with Org Agenda searches40244 +Node: org-sidebar41115 +Node: License41394  End Tag Table From 77702850026722ebea730e2124288e826060180e Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Mon, 16 Nov 2020 02:37:49 -0600 Subject: [PATCH 034/372] Fix: (org-ql-view--contract-buffers-files) Non-existent org-directory Fixes #138. Fixes #139. Thanks to Matt Huszagh (@matthuszagh) for reporting. This *actually* fixes those issues. The 0.4.7 release is related to those issues, but those issues actually arose in code added to 0.5-pre. --- org-ql-view.el | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/org-ql-view.el b/org-ql-view.el index 8e8d40b..e44c658 100644 --- a/org-ql-view.el +++ b/org-ql-view.el @@ -1039,7 +1039,9 @@ current buffer. Otherwise BUFFERS-FILES is returned unchanged." (pcase (expand-files buffers-files) ((pred (seq-set-equal-p (mapcar #'expand-file-name (org-agenda-files)))) "org-agenda-files") - ((pred (seq-set-equal-p (org-ql-search-directories-files))) + ((and (guard (file-exists-p org-directory)) + (pred (seq-set-equal-p (org-ql-search-directories-files + :directories (list org-directory))))) "org-directory") (_ buffers-files))) ((pred (equal (current-buffer))) From e9815f39a071670981d2afcf4bb4b68b034a6765 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Mon, 16 Nov 2020 03:16:40 -0600 Subject: [PATCH 035/372] Tests: Fix link-opening test --- tests/test-org-ql.el | 51 +++++++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/tests/test-org-ql.el b/tests/test-org-ql.el index ee9b772..3902187 100644 --- a/tests/test-org-ql.el +++ b/tests/test-org-ql.el @@ -1415,22 +1415,31 @@ RESULTS should be a list of strings as returned by (insert "* TODO Test heading\n\n") (org-mode))) - (cl-flet ((var-after-link-save-open - (var buffers-files query &key sort super-groups (input "RET")) - (org-ql-search buffers-files query - :super-groups super-groups - :sort sort :title title :buffer view-buffer) - (with-current-buffer view-buffer - (with-simulated-input input - ;; Avoid writing "Stored: ..." to test output. - (let ((inhibit-message t)) - (org-store-link nil))) - (kill-buffer)) - ;; ;; Let's see if this trick works. - (org-open-link-from-string (caar org-stored-links) nil link-buffer) - (with-current-buffer (get-buffer (concat "*Org QL View: " title "*")) - (prog1 (buffer-local-value var (current-buffer)) - (kill-buffer))))) + (cl-flet* ((open-link-in + (link buffer) + ;; Org REDUCED THE NUMBER OF ARGUMENTS TO `org-open-link-from-string'! That BREAKS BACKWARD + ;; COMPATIBILITY! So I have to make my own function so these tests can work across Org versions! + (with-current-buffer buffer + (erase-buffer) + (org-mode) + (insert link) + (backward-char 1) + (org-open-at-point))) + (var-after-link-save-open + (var buffers-files query &key sort super-groups (buffer link-buffer) (input "RET")) + (org-ql-search buffers-files query + :super-groups super-groups + :sort sort :title title :buffer view-buffer) + (with-current-buffer view-buffer + (with-simulated-input input + ;; Avoid writing "Stored: ..." to test output. + (let ((inhibit-message t)) + (org-store-link nil))) + (kill-buffer)) + (open-link-in (caar org-stored-links) buffer) + (with-current-buffer (get-buffer (concat "*Org QL View: " title "*")) + (prog1 (buffer-local-value var (current-buffer)) + (kill-buffer))))) (describe "Queries" :var ((string-query "todo:TODO regexp:heading") @@ -1465,9 +1474,6 @@ RESULTS should be a list of strings as returned by (describe "Buffers/Files" :var ((query '(and (todo "TODO") (regexp "heading"))) (one-filename (car temp-filenames))) - (it "Can search buffer containing the link" - (expect (var-after-link-save-open 'org-ql-view-buffers-files one-filename query) - :to-equal link-buffer)) (it "Can search a file by filename" (expect (var-after-link-save-open 'org-ql-view-buffers-files one-filename query :input "M-n M-n RET") @@ -1475,7 +1481,12 @@ RESULTS should be a list of strings as returned by (it "Can search multiple files by filename" (expect (var-after-link-save-open 'org-ql-view-buffers-files temp-filenames query :input "M-n M-n RET") - :to-equal temp-filenames))))) + :to-equal temp-filenames)) + (it "Can search buffer containing the link" + ;; This is sort-of a special case because of how the test link-opening function works. + (expect (var-after-link-save-open 'org-ql-view-buffers-files one-filename query + :buffer link-buffer) + :to-equal link-buffer))))) ;; MAYBE: Also test `org-ql-views', although I already know it works now. ;; (describe "org-ql-views") From c3aa701e57c6bc3a3d7d4701c4a5d6e07de68f33 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Mon, 16 Nov 2020 03:29:10 -0600 Subject: [PATCH 036/372] Tidy: Silence, Checkdoc! Some of these warnings are too much. --- org-ql-search.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/org-ql-search.el b/org-ql-search.el index c43573d..a9fcf7d 100644 --- a/org-ql-search.el +++ b/org-ql-search.el @@ -334,7 +334,7 @@ For example, an org-ql dynamic block header could look like: (cl-defun org-ql-search-directories-files (&key (directories (if (file-exists-p org-directory) (list org-directory) - (user-error "org-ql-search-directories-files: No DIRECTORIES given, and `org-directory' doesn't exist"))) + (user-error "Org-ql-search-directories-files: No DIRECTORIES given, and `org-directory' doesn't exist"))) (recurse org-ql-search-directories-files-recursive) (regexp org-ql-search-directories-files-regexp)) "Return list of matching files in DIRECTORIES, a list of directory paths. From faaa6f7ee92fc33e9c873d9661a86c563bd72e86 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Mon, 16 Nov 2020 03:49:15 -0600 Subject: [PATCH 037/372] Test: Tidy This prevents extra output about buffers without headings. --- tests/test-org-ql.el | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test-org-ql.el b/tests/test-org-ql.el index 3902187..9181500 100644 --- a/tests/test-org-ql.el +++ b/tests/test-org-ql.el @@ -1422,6 +1422,7 @@ RESULTS should be a list of strings as returned by (with-current-buffer buffer (erase-buffer) (org-mode) + (insert "* TODO Test heading\n\n") (insert link) (backward-char 1) (org-open-at-point))) From c07971cd57782e0d6539c083c686364a1ab212a8 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Mon, 16 Nov 2020 03:49:46 -0600 Subject: [PATCH 038/372] Tidy: Use -let* instead of pcase-let* for plists This actually only fixes a problem with running the tests, because I don't have a way to force map-2.1 to be installed into the sandbox, so the tests on CI always fail. So for now we just use -let* for this. --- org-ql-search.el | 58 ++++++++++++++++++++++++------------------------ org-ql-view.el | 22 +++++++++--------- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/org-ql-search.el b/org-ql-search.el index a9fcf7d..e7758c7 100644 --- a/org-ql-search.el +++ b/org-ql-search.el @@ -272,35 +272,35 @@ Valid parameters include: For example, an org-ql dynamic block header could look like: #+BEGIN: org-ql :query (todo \"UNDERWAY\") :columns (priority todo heading) :sort (priority date) :ts-format \"%Y-%m-%d %H:%M\"" - (pcase-let* (((map :query :columns :sort :ts-format :take) params) - (query (cl-etypecase query - (string (org-ql--plain-query query)) - (t query))) - (columns (or columns '(heading todo (priority "P")))) - ;; MAYBE: Custom column functions. - (format-fns - ;; NOTE: Backquoting this alist prevents the lambdas from seeing - ;; the variable `ts-format', so we use `list' and `cons'. - (list (cons 'todo (lambda (element) - (org-element-property :todo-keyword element))) - (cons 'heading (lambda (element) - (org-make-link-string (org-element-property :raw-value element) - (org-element-property :raw-value element)))) - (cons 'priority (lambda (element) - (--when-let (org-element-property :priority element) - (char-to-string it)))) - (cons 'deadline (lambda (element) - (--when-let (org-element-property :deadline element) - (ts-format ts-format (ts-parse-org-element it))))) - (cons 'scheduled (lambda (element) - (--when-let (org-element-property :scheduled element) - (ts-format ts-format (ts-parse-org-element it))))) - (cons 'property (lambda (element property) - (org-element-property (intern (concat ":" (upcase property))) element))))) - (elements (org-ql-query :from (current-buffer) - :where query - :select '(org-element-headline-parser (line-end-position)) - :order-by sort))) + (-let* (((&plist :query :columns :sort :ts-format :take) params) + (query (cl-etypecase query + (string (org-ql--plain-query query)) + (t query))) + (columns (or columns '(heading todo (priority "P")))) + ;; MAYBE: Custom column functions. + (format-fns + ;; NOTE: Backquoting this alist prevents the lambdas from seeing + ;; the variable `ts-format', so we use `list' and `cons'. + (list (cons 'todo (lambda (element) + (org-element-property :todo-keyword element))) + (cons 'heading (lambda (element) + (org-make-link-string (org-element-property :raw-value element) + (org-element-property :raw-value element)))) + (cons 'priority (lambda (element) + (--when-let (org-element-property :priority element) + (char-to-string it)))) + (cons 'deadline (lambda (element) + (--when-let (org-element-property :deadline element) + (ts-format ts-format (ts-parse-org-element it))))) + (cons 'scheduled (lambda (element) + (--when-let (org-element-property :scheduled element) + (ts-format ts-format (ts-parse-org-element it))))) + (cons 'property (lambda (element property) + (org-element-property (intern (concat ":" (upcase property))) element))))) + (elements (org-ql-query :from (current-buffer) + :where query + :select '(org-element-headline-parser (line-end-position)) + :order-by sort))) (when take (setf elements (cl-etypecase take ((and integer (satisfies cl-minusp)) (-take-last (abs take) elements)) diff --git a/org-ql-view.el b/org-ql-view.el index e44c658..32d6866 100644 --- a/org-ql-view.el +++ b/org-ql-view.el @@ -550,8 +550,8 @@ dates in the past, and negative for dates in the future." (when (buffer-base-buffer b-f) (buffer-file-name (buffer-base-buffer b-f))))) (t (user-error "Only file-backed buffers can be bookmarked by Org QL View: %s" b-f))))) - (pcase-let* ((plist (org-ql-view--plist (current-buffer))) - ((map :buffers-files) plist)) + (-let* ((plist (org-ql-view--plist (current-buffer))) + ((&plist :buffers-files) plist)) ;; Replace buffers with their filenames, and signal error if any are not file-backed. (setf plist (plist-put plist :buffers-files (cl-etypecase buffers-files @@ -566,15 +566,15 @@ dates in the past, and negative for dates in the future." ;;;###autoload (defun org-ql-view-bookmark-handler (bookmark) "Show Org QL View BOOKMARK in current buffer." - ;; FIXME: Getting "(void-variable super-groups)" errors when this function is byte-compiled, - ;; but not when it's interpreted! Using (:super-groups super-groups) instead of just - ;; :super-groups fixes it, but I have no idea why, and the macroexpansion is the same. - (pcase-let* (((map org-ql-view-plist) (bookmark-get-bookmark-record bookmark)) - ((map :buffers-files :query :super-groups :narrow :sort :title) - org-ql-view-plist) - (super-groups (cl-etypecase super-groups - (symbol (symbol-value super-groups)) - (list super-groups)))) + ;; FIXME: `pcase-let*' is easier to use to destructure this, but if I use + ;; that, I want to use map 2.1 for the extra convenience, but I can't force + ;; that to be installed into the makem.sh sandbox, so I just use `-let*' here. + (-let* ((org-ql-view-plist (car (bookmark-get-bookmark-record bookmark))) + ((&plist :buffers-files :query :super-groups :narrow :sort :title) + (cdr org-ql-view-plist)) + (super-groups (cl-etypecase super-groups + (symbol (symbol-value super-groups)) + (list super-groups)))) (org-ql-search buffers-files query :super-groups super-groups :narrow narrow :sort sort :title title) ;; HACK: `bookmark--jump-via' expects that, when the handler returns, the current buffer From 5814cff18840a29a3dd08f4128b971fa50c6a778 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Mon, 16 Nov 2020 03:52:26 -0600 Subject: [PATCH 039/372] Tests: (test.yml) Let lint fail Some of these lint warnings aren't going to be fixed anytime soon, and it's more useful to let the tests indicate success in that case. --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c3ec60f..f9ead5f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,7 +59,7 @@ jobs: - name: Lint # NOTE: Uncomment this line to treat lint failures as passing # so the job doesn't show failure. - # continue-on-error: true + continue-on-error: true run: ./makem.sh -vv --sandbox=$SANDBOX_DIR lint - name: Test From 12b3b426b563627c6a1068df245b6aa86646e931 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Mon, 16 Nov 2020 04:49:20 -0600 Subject: [PATCH 040/372] Tests: Call org-store-link interactively --- tests/test-org-ql.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test-org-ql.el b/tests/test-org-ql.el index 9181500..f1c1f25 100644 --- a/tests/test-org-ql.el +++ b/tests/test-org-ql.el @@ -1435,7 +1435,7 @@ RESULTS should be a list of strings as returned by (with-simulated-input input ;; Avoid writing "Stored: ..." to test output. (let ((inhibit-message t)) - (org-store-link nil))) + (call-interactively #'org-store-link nil))) (kill-buffer)) (open-link-in (caar org-stored-links) buffer) (with-current-buffer (get-buffer (concat "*Org QL View: " title "*")) From 9396385fed8253f6378da1126ed756a44058c2ef Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Mon, 16 Nov 2020 04:49:28 -0600 Subject: [PATCH 041/372] Tests: Add a couple of asserts Not absolutely needed now, but they might help in the future if something changes again. --- tests/test-org-ql.el | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test-org-ql.el b/tests/test-org-ql.el index f1c1f25..ed3ef06 100644 --- a/tests/test-org-ql.el +++ b/tests/test-org-ql.el @@ -1432,11 +1432,15 @@ RESULTS should be a list of strings as returned by :super-groups super-groups :sort sort :title title :buffer view-buffer) (with-current-buffer view-buffer + (cl-assert (member '("org-ql-search" :follow org-ql-view--link-open :store org-ql-view--link-store) + org-link-parameters) + t) (with-simulated-input input ;; Avoid writing "Stored: ..." to test output. (let ((inhibit-message t)) (call-interactively #'org-store-link nil))) (kill-buffer)) + (cl-assert (and org-stored-links (caar org-stored-links)) t) (open-link-in (caar org-stored-links) buffer) (with-current-buffer (get-buffer (concat "*Org QL View: " title "*")) (prog1 (buffer-local-value var (current-buffer)) From 673edaca11db1879d36297b96f7ea648f7125b74 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Mon, 16 Nov 2020 04:52:45 -0600 Subject: [PATCH 042/372] Fix: (org-ql-view--link-open) Handle Org 9.3 links Or try to, anyway... --- org-ql-view.el | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/org-ql-view.el b/org-ql-view.el index 32d6866..10519c2 100644 --- a/org-ql-view.el +++ b/org-ql-view.el @@ -610,6 +610,14 @@ PATH should be the part of an \"org-ql-search:\" URL after the protocol. See, e.g. `org-ql-view--link-store'." (require 'url-parse) (require 'url-util) + (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))) (pcase-let* ((`(,query . ,params) (url-path-and-query (url-parse-make-urlobj "org-ql-search" nil nil nil nil path))) From badf214db7c3032052c219de5733abef83e55635 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Mon, 16 Nov 2020 05:01:33 -0600 Subject: [PATCH 043/372] Tests: Adjust error message for Org 9.3 and/or Emacs 27.1 I don't know if this is due to the Org 9.3 link change or if something print/read-related changed in Emacs 27.1, but this should fix the test, hopefully on both versions. --- tests/test-org-ql.el | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/test-org-ql.el b/tests/test-org-ql.el index ed3ef06..be81d81 100644 --- a/tests/test-org-ql.el +++ b/tests/test-org-ql.el @@ -1205,8 +1205,11 @@ RESULTS should be a list of strings as returned by (expect (open-link unquoted-lambda-link) :to-throw 'error '("CAUTION: Link not opened because unsafe buffers-files parameter detected: (lambda nil (error UNSAFE))"))) (it "Errors for a quoted lambda in a list" - (expect (open-link quoted-lambda-in-list-link) - :to-throw 'error '("CAUTION: Link not opened because unsafe buffers-files parameter detected: ((quote (lambda nil (error UNSAFE))))"))) + (if (version< (org-version) "9.3") + (expect (open-link quoted-lambda-in-list-link) + :to-throw 'error '("CAUTION: Link not opened because unsafe buffers-files parameter detected: ((quote (lambda nil (error UNSAFE))))")) + (expect (open-link quoted-lambda-in-list-link) + :to-throw 'error '("CAUTION: Link not opened because unsafe buffers-files parameter detected: ('(lambda nil (error UNSAFE)))")))) (it "Errors for an unquoted lambda in a list" (expect (open-link unquoted-lambda-in-list-link) :to-throw 'error '("CAUTION: Link not opened because unsafe buffers-files parameter detected: ((lambda nil (error UNSAFE)))")))) @@ -1268,8 +1271,11 @@ RESULTS should be a list of strings as returned by (expect (open-link unquoted-lambda-link) :to-throw 'error '("CAUTION: Link not opened because unsafe sort parameter detected: (lambda (_ _) (error UNSAFE))"))) (it "Errors for a quoted lambda in a list" - (expect (open-link quoted-lambda-in-list-link) - :to-throw 'error '("CAUTION: Link not opened because unsafe sort parameter detected: ((quote (lambda (_ _) (error UNSAFE))))"))) + (if (version< (org-version) "9.3") + (expect (open-link quoted-lambda-in-list-link) + :to-throw 'error '("CAUTION: Link not opened because unsafe sort parameter detected: ((quote (lambda (_ _) (error UNSAFE))))")) + (expect (open-link quoted-lambda-in-list-link) + :to-throw 'error '("CAUTION: Link not opened because unsafe sort parameter detected: ('(lambda (_ _) (error UNSAFE)))")))) (it "Errors for an unquoted lambda in a list" (expect (open-link unquoted-lambda-in-list-link) :to-throw 'error '("CAUTION: Link not opened because unsafe sort parameter detected: ((lambda nil (error UNSAFE)))"))))))) From 244c8290d533e5d42c8cbc2c6287889caa9e4a3e Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Mon, 16 Nov 2020 05:13:05 -0600 Subject: [PATCH 044/372] Notes: Update --- notes.org | 199 +++++++++++++++++++++++++++--------------------------- 1 file changed, 100 insertions(+), 99 deletions(-) diff --git a/notes.org b/notes.org index c15d42a..98cb689 100644 --- a/notes.org +++ b/notes.org @@ -1,5 +1,5 @@ #+PROPERTY: LOGGING nil -#+TODO: TODO MAYBE NEXT PROJECT UNDERWAY WAITING | DONE CANCELED +#+TODO: TODO MAYBE NEXT PROJECT UNDERWAY WAITING | DONE(d) CANCELED * [#A] Contents :PROPERTIES: @@ -23,9 +23,7 @@ #+BEGIN: org-ql :query "todo: tags:bug" :columns ((priority "P") ((property "milestone") "M") (todo "Keyword") heading) :sort (priority todo date) :ts-format "%Y-%m-%d %H:%M" | P | M | Keyword | Heading | |---+-----+---------+------------------------------------------------| -| A | 0.5 | NEXT | [[Write tests for storing/opening links][Write tests for storing/opening links]] | | A | 0.5 | PROJECT | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | -| A | 0.5 | PROJECT | [[Fix =org-ql-view--link-open= on Org 9.3+][Fix =org-ql-view--link-open= on Org 9.3+]] | #+END: ** Underway ([[org-ql-search:todo%253AUNDERWAY?sort=%2528priority%2529&title=%2522Underway%2522][view]]) @@ -42,13 +40,13 @@ #+BEGIN: org-ql :query "todo: priority:A" :columns ((priority "P") ((property "milestone") "M") todo heading) :sort (priority date) :take 7 | P | M | Todo | Heading | |---+-----+---------+--------------------------------------------------------------| +| A | 0.5 | TODO | [[Add Emacs 27.1 to =test.yml=][Add Emacs 27.1 to =test.yml=]] | | A | | NEXT | [[Make dynamic blocks warn about sexp queries][Make dynamic blocks warn about sexp queries]] | | A | 0.5 | PROJECT | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | -| A | 0.5 | PROJECT | [[Fix =org-ql-view--link-open= on Org 9.3+][Fix =org-ql-view--link-open= on Org 9.3+]] | -| A | 0.5 | NEXT | [[Write tests for storing/opening links][Write tests for storing/opening links]] | | A | | PROJECT | [[Convert simple sexp queries to non-sexp][Convert simple sexp queries to non-sexp]] | | A | | PROJECT | [[Checking links for unsafe parameters][Checking links for unsafe parameters]] | | A | 0.5 | TODO | [[Tag org-super-agenda 1.2 and bump required version in org-ql][Tag org-super-agenda 1.2 and bump required version in org-ql]] | +| A | | PROJECT | [[Org%20link%20types%20%5B2/3%5D][Org link types {2/3}]] | #+END: ** Stuck projects ([[org-ql-search:(and%20(todo%20"PROJECT")%20(not%20(descendants%20(todo%20"NEXT"))))?super-groups=((:auto-parent))&sort=(priority)&title="Stuck%20Projects"][view]]) @@ -70,9 +68,8 @@ #+BEGIN: org-ql :query "todo: property:milestone" :columns (((property "milestone") "M") (priority "P") todo heading) :sort (priority date) :take 7 | M | P | Todo | Heading | |-----+---+---------+--------------------------------------------------------------| +| 0.5 | A | TODO | [[Add Emacs 27.1 to =test.yml=][Add Emacs 27.1 to =test.yml=]] | | 0.5 | A | PROJECT | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | -| 0.5 | A | PROJECT | [[Fix =org-ql-view--link-open= on Org 9.3+][Fix =org-ql-view--link-open= on Org 9.3+]] | -| 0.5 | A | NEXT | [[Write tests for storing/opening links][Write tests for storing/opening links]] | | 0.5 | A | TODO | [[Tag org-super-agenda 1.2 and bump required version in org-ql][Tag org-super-agenda 1.2 and bump required version in org-ql]] | | 0.6 | B | PROJECT | [[Save view to dynamic block][Save view to dynamic block]] | #+END: @@ -96,7 +93,6 @@ - [[#fancier-searching-for-inherited-tags][Fancier searching for inherited tags]] - [[#make-dynamic-blocks-warn-about-sexp-queries][Make dynamic blocks warn about sexp queries]] - [[#compatibility-with-org-94-custom-link-changes][Compatibility with Org 9.4 custom link changes]] -- [[#fix-org-ql-view--link-open-on-org-93][Fix org-ql-view--link-open on Org 9.3+]] - [[#convert-simple-sexp-queries-to-non-sexp][Convert simple sexp queries to non-sexp]] - [[#checking-links-for-unsafe-parameters][Checking links for unsafe parameters]] - [[#move-this-notes-file-into-an-orphan-metanotes-branch][Move this notes file into an orphan meta/notes branch]] @@ -113,6 +109,7 @@ - [[#update-view-screenshots][Update view screenshots]] - [[#test-caching][Test caching]] - [[#node-caching]["Node" caching]] +- [[#fix-org-ql-view--link-open-on-org-93][Fix org-ql-view--link-open on Org 9.3+]] - [[#fix-query-sexp-to-string-functions-handling-of-eg-descendants][Fix query-sexp-to-string function's handling of, e.g. descendants]] - [[#helm-command][Helm command]] - [[#quickly-change-sortinggrouping-in-search-views][Quickly change sorting/grouping in search views]] @@ -262,97 +259,6 @@ Eventually, the function org-export-custom-protocol-maybe is now called with a f Unfortunately it does not say what the new, required second argument is. -** PROJECT [#A] Fix =org-ql-view--link-open= on Org 9.3+ :compatibility:bug: -:PROPERTIES: -:milestone: 0.5 -:url: https://github.com/alphapapa/org-ql/issues/147 -:END: - -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=: - -#+BEGIN_SRC elisp - (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))) -#+END_SRC - -But, first, I should write tests for saving and opening links, so it can actually be tested on different versions automatically. - -*** NEXT [#A] Write tests for storing/opening links -:PROPERTIES: -:milestone: 0.5 -:END: - -*** DONE [#A] Check Org release notes for link changes - -[2020-11-13 Fri 22:44] From [[https://www.orgmode.org/Changes_old.html][the changelog]]: - -#+BEGIN_QUOTE -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. -#+END_QUOTE - -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? - - ** 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... @@ -1495,6 +1401,101 @@ This works okay (except the priority accessor needs to be fixed, because Org pri :action #'helm-org-goto-marker)))) #+END_SRC +** DONE [#A] Fix =org-ql-view--link-open= on Org 9.3+ :compatibility:bug: +:PROPERTIES: +:milestone: 0.5 +:url: https://github.com/alphapapa/org-ql/issues/147 +:END: + +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=: + +#+BEGIN_SRC elisp + (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))) +#+END_SRC + +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. + +*** DONE [#A] Write tests for storing/opening links +:PROPERTIES: +:milestone: 0.5 +:END: + +[2020-11-16 Mon 05:11] Took way longer than I expected. I hope it was worth it. + +*** DONE [#A] Check Org release notes for link changes + +[2020-11-13 Fri 22:44] From [[https://www.orgmode.org/Changes_old.html][the changelog]]: + +#+BEGIN_QUOTE +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. +#+END_QUOTE + +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? + + ** DONE [#A] Fix query-sexp-to-string function's handling of, e.g. =descendants= :bug: :PROPERTIES: :milestone: 0.5 From ec0f8c7231132056842941d1b8ac7baea3cf4026 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Mon, 16 Nov 2020 05:16:56 -0600 Subject: [PATCH 045/372] Meta: Update makem.sh, Makefile, test.yml --- .github/workflows/test.yml | 8 ++++++-- Makefile | 3 ++- makem.sh | 3 ++- notes.org | 16 ++++++++-------- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f9ead5f..0b274ed 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,9 @@ # * test.yml --- Test Emacs packages using makem.sh on GitHub Actions -# https://github.com/alphapapa/makem.sh +# URL: https://github.com/alphapapa/makem.sh +# Version: 0.2 + +# * Commentary: # Based on Steve Purcell's examples at # , @@ -39,6 +42,7 @@ jobs: matrix: emacs_version: - 26.3 + - 27.1 - snapshot steps: - uses: purcell/setup-emacs@master @@ -50,7 +54,7 @@ jobs: - name: Initialize sandbox run: | SANDBOX_DIR=$(mktemp -d) || exit 1 - echo ::set-env name=SANDBOX_DIR::$SANDBOX_DIR + echo "SANDBOX_DIR=$SANDBOX_DIR" >> $GITHUB_ENV ./makem.sh -vv --sandbox=$SANDBOX_DIR --install-deps --install-linters # The "all" rule is not used, because it treats compilation warnings diff --git a/Makefile b/Makefile index 17abfe0..68906e9 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ # * makem.sh/Makefile --- Script to aid building and testing Emacs Lisp packages -# This Makefile is from the makem.sh repo: . +# URL: https://github.com/alphapapa/makem.sh +# Version: 0.2 # * Arguments diff --git a/makem.sh b/makem.sh index d110e2c..2d710a3 100755 --- a/makem.sh +++ b/makem.sh @@ -2,7 +2,8 @@ # * makem.sh --- Script to aid building and testing Emacs Lisp packages -# https://github.com/alphapapa/makem.sh +# URL: https://github.com/alphapapa/makem.sh +# Version: 0.2 # * Commentary: diff --git a/notes.org b/notes.org index 98cb689..f8b5d9d 100644 --- a/notes.org +++ b/notes.org @@ -40,7 +40,6 @@ #+BEGIN: org-ql :query "todo: priority:A" :columns ((priority "P") ((property "milestone") "M") todo heading) :sort (priority date) :take 7 | P | M | Todo | Heading | |---+-----+---------+--------------------------------------------------------------| -| A | 0.5 | TODO | [[Add Emacs 27.1 to =test.yml=][Add Emacs 27.1 to =test.yml=]] | | A | | NEXT | [[Make dynamic blocks warn about sexp queries][Make dynamic blocks warn about sexp queries]] | | A | 0.5 | PROJECT | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | | A | | PROJECT | [[Convert simple sexp queries to non-sexp][Convert simple sexp queries to non-sexp]] | @@ -68,7 +67,6 @@ #+BEGIN: org-ql :query "todo: property:milestone" :columns (((property "milestone") "M") (priority "P") todo heading) :sort (priority date) :take 7 | M | P | Todo | Heading | |-----+---+---------+--------------------------------------------------------------| -| 0.5 | A | TODO | [[Add Emacs 27.1 to =test.yml=][Add Emacs 27.1 to =test.yml=]] | | 0.5 | A | PROJECT | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | | 0.5 | A | TODO | [[Tag org-super-agenda 1.2 and bump required version in org-ql][Tag org-super-agenda 1.2 and bump required version in org-ql]] | | 0.6 | B | PROJECT | [[Save view to dynamic block][Save view to dynamic block]] | @@ -79,7 +77,6 @@ :TOC: :include descendants :depth 1 :END: :CONTENTS: -- [[#add-emacs-271-to-testyml][Add Emacs 27.1 to test.yml]] - [[#add-auto-keyword-to-planning-predicate][Add :auto keyword to (planning) predicate]] - [[#change-deadlines-auto-argument-to-auto-andor-auto-t][Change (deadline)'s auto argument to :auto and/or :auto t]] - [[#outline-path-in-buffers-files-arg][Outline path in buffers-files arg]] @@ -109,6 +106,7 @@ - [[#update-view-screenshots][Update view screenshots]] - [[#test-caching][Test caching]] - [[#node-caching]["Node" caching]] +- [[#add-emacs-271-to-testyml][Add Emacs 27.1 to test.yml]] - [[#fix-org-ql-view--link-open-on-org-93][Fix org-ql-view--link-open on Org 9.3+]] - [[#fix-query-sexp-to-string-functions-handling-of-eg-descendants][Fix query-sexp-to-string function's handling of, e.g. descendants]] - [[#helm-command][Helm command]] @@ -120,11 +118,6 @@ - [[#use-macros-for-date][Use macros for date]] :END: -** TODO [#A] Add Emacs 27.1 to =test.yml= -:PROPERTIES: -:milestone: 0.5 -:END: - ** TODO [#B] Add ~:auto~ keyword to ~(planning)~ predicate It should act like ~(or (deadline auto) (scheduled :to today))~. @@ -1401,6 +1394,13 @@ This works okay (except the priority accessor needs to be fixed, because Org pri :action #'helm-org-goto-marker)))) #+END_SRC +** DONE [#A] Add Emacs 27.1 to =test.yml= +:PROPERTIES: +:milestone: 0.5 +:END: + +[2020-11-16 Mon 05:22] Also releasing =makem.sh= 0.2 with this change. + ** DONE [#A] Fix =org-ql-view--link-open= on Org 9.3+ :compatibility:bug: :PROPERTIES: :milestone: 0.5 From 2c14903c7654fddeedf387937607aa2e1baaea60 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Mon, 16 Nov 2020 05:50:37 -0600 Subject: [PATCH 046/372] Notes: Update --- notes.org | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/notes.org b/notes.org index f8b5d9d..1f87eb7 100644 --- a/notes.org +++ b/notes.org @@ -40,26 +40,26 @@ #+BEGIN: org-ql :query "todo: priority:A" :columns ((priority "P") ((property "milestone") "M") todo heading) :sort (priority date) :take 7 | P | M | Todo | Heading | |---+-----+---------+--------------------------------------------------------------| -| A | | NEXT | [[Make dynamic blocks warn about sexp queries][Make dynamic blocks warn about sexp queries]] | +| A | 0.5 | NEXT | [[Make dynamic blocks warn about sexp queries][Make dynamic blocks warn about sexp queries]] | | A | 0.5 | PROJECT | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | -| A | | PROJECT | [[Convert simple sexp queries to non-sexp][Convert simple sexp queries to non-sexp]] | -| A | | PROJECT | [[Checking links for unsafe parameters][Checking links for unsafe parameters]] | +| A | 0.5 | PROJECT | [[Convert simple sexp queries to non-sexp][Convert simple sexp queries to non-sexp]] | +| A | 0.5 | PROJECT | [[Checking links for unsafe parameters][Checking links for unsafe parameters]] | | A | 0.5 | TODO | [[Tag org-super-agenda 1.2 and bump required version in org-ql][Tag org-super-agenda 1.2 and bump required version in org-ql]] | | A | | PROJECT | [[Org%20link%20types%20%5B2/3%5D][Org link types {2/3}]] | #+END: -** Stuck projects ([[org-ql-search:(and%20(todo%20"PROJECT")%20(not%20(descendants%20(todo%20"NEXT"))))?super-groups=((:auto-parent))&sort=(priority)&title="Stuck%20Projects"][view]]) +** Stuck projects ([[org-ql-search:%2528and%2520%2528todo%2520%2522PROJECT%2522%2529%2520%2528not%2520%2528descendants%2520%2528todo%2520%2522NEXT%2522%2529%2529%2529%2529?super-groups=%2528%2528%253Aauto-property%2520%2522milestone%2522%2529%2529&sort=%2528priority%2529&title=%2522Stuck%2520Projects%2522][view]]) #+BEGIN: org-ql :query (and (todo "PROJECT") (not (descendants (todo "NEXT")))) :columns (((property "milestone") "M") (priority "P") heading) :sort (priority date) :take 7 | M | P | Heading | |-----+---+-------------------------------------------------------| | 0.5 | A | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | -| | A | [[Convert simple sexp queries to non-sexp][Convert simple sexp queries to non-sexp]] | -| | A | [[Checking links for unsafe parameters][Checking links for unsafe parameters]] | +| 0.5 | A | [[Convert simple sexp queries to non-sexp][Convert simple sexp queries to non-sexp]] | +| 0.5 | A | [[Checking links for unsafe parameters][Checking links for unsafe parameters]] | | | A | [[Org%20link%20types%20%5B2/3%5D][Org link types {2/3}]] | | | B | [[Move this notes file into an orphan =meta/notes= branch][Move this notes file into an orphan =meta/notes= branch]] | | | B | [[Outline path predicate][Outline path predicate]] | -| | B | [[Group tag support][Group tag support]] | +| 0.6 | B | [[Group tag support][Group tag support]] | #+END: ** Milestones ([[org-ql-search:todo:?super-groups=((:auto-property%20"milestone"))&sort=(todo)&title="Milestones"][view]]) @@ -67,8 +67,12 @@ #+BEGIN: org-ql :query "todo: property:milestone" :columns (((property "milestone") "M") (priority "P") todo heading) :sort (priority date) :take 7 | M | P | Todo | Heading | |-----+---+---------+--------------------------------------------------------------| +| 0.5 | A | NEXT | [[Make dynamic blocks warn about sexp queries][Make dynamic blocks warn about sexp queries]] | | 0.5 | A | PROJECT | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | +| 0.5 | A | PROJECT | [[Convert simple sexp queries to non-sexp][Convert simple sexp queries to non-sexp]] | +| 0.5 | A | PROJECT | [[Checking links for unsafe parameters][Checking links for unsafe parameters]] | | 0.5 | A | TODO | [[Tag org-super-agenda 1.2 and bump required version in org-ql][Tag org-super-agenda 1.2 and bump required version in org-ql]] | +| 0.6 | B | PROJECT | [[Group tag support][Group tag support]] | | 0.6 | B | PROJECT | [[Save view to dynamic block][Save view to dynamic block]] | #+END: @@ -220,6 +224,9 @@ I should benchmark it to see how much difference it makes, because all those ~fs However, there might still be a useful idea here somewhere... ** MAYBE [#C] Alternative parsing libraries +:PROPERTIES: +:milestone: future +:END: + e.g. Bovine and Wisent come with Emacs, which would allow us to drop the =peg= dependency (which doesn't use lexical binding). + [[https://github.com/cute-jumper/parsec.el][parsec]] is third-party, but looks powerful @@ -229,6 +236,9 @@ However, there might still be a useful idea here somewhere... 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. ** NEXT [#A] Make dynamic blocks warn about sexp queries +:PROPERTIES: +:milestone: 0.5 +:END: [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. @@ -253,6 +263,9 @@ Unfortunately it does not say what the new, required second argument is. ** PROJECT [#A] Convert simple sexp queries to non-sexp +:PROPERTIES: +:milestone: 0.5 +:END: [2020-11-11 Wed 00:28] This will be very helpful for storing links. Surely simple ones won't be too hard... @@ -357,6 +370,7 @@ Maybe make it an option to automatically convert them when possible, because if ** PROJECT [#A] Checking links for unsafe parameters :PROPERTIES: :ID: ba70e375-eddb-40df-8892-fb418c1f70d1 +:milestone: 0.5 :END: :LOGBOOK: - State "PROJECT" from "UNDERWAY" [2020-11-12 Thu 00:26] @@ -644,6 +658,9 @@ That seems to work, like: *** DONE Save query from ql-agenda buffer ** PROJECT [#B] Group tag support +:PROPERTIES: +:milestone: 0.6 +:END: *** UNDERWAY Benchmarking tags searches without and with new group-tags support @@ -1138,6 +1155,9 @@ One of the things in that branch is =org-ql-item=, which is a struct used to car 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= +:PROPERTIES: +:milestone: future +:END: 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. @@ -4205,6 +4225,6 @@ I don't need this right now, but it might come in handy here or elsewhere. ** File-local variables # Local Variables: -# before-save-hook: ((lambda () (when (fboundp 'unpackaged/org-fix-blank-lines) (unpackaged/org-fix-blank-lines t))) (lambda () (when (fboundp 'ap/org-sort-entries-recursive-multi) (save-excursion (goto-char (point-min)) (ap/org-sort-entries-recursive-multi '(?a ?p ?o))))) org-make-toc) +# before-save-hook: ((lambda () (when (fboundp 'unpackaged/org-fix-blank-lines) (unpackaged/org-fix-blank-lines t))) (lambda () (when (fboundp 'ap/org-sort-entries-recursive-multi) (save-excursion (goto-char (point-min)) (ap/org-sort-entries-recursive-multi '(?a ?p ?o))))) org-update-all-dblocks org-make-toc) # End: From 2a010f29c18a32ebe1d71d56ce76d0b1631838c9 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Mon, 16 Nov 2020 20:08:44 -0600 Subject: [PATCH 047/372] Fix: (org-dblock-write:org-ql) Use org-link-display-format Fixes #153. Thanks to @jbriquet for reporting. --- org-ql-search.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/org-ql-search.el b/org-ql-search.el index e7758c7..9f555ce 100644 --- a/org-ql-search.el +++ b/org-ql-search.el @@ -285,7 +285,8 @@ For example, an org-ql dynamic block header could look like: (org-element-property :todo-keyword element))) (cons 'heading (lambda (element) (org-make-link-string (org-element-property :raw-value element) - (org-element-property :raw-value element)))) + (org-link-display-format + (org-element-property :raw-value element))))) (cons 'priority (lambda (element) (--when-let (org-element-property :priority element) (char-to-string it)))) From 50ba4414155a27d33c4c1a31de8b2f9fb90ad669 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Mon, 16 Nov 2020 21:16:09 -0600 Subject: [PATCH 048/372] Fix: (org-ql-view-bookmark-handler) Bookmark destructuring See #147. Thanks to @tpeacock19 for reporting. --- org-ql-view.el | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/org-ql-view.el b/org-ql-view.el index 10519c2..29e5499 100644 --- a/org-ql-view.el +++ b/org-ql-view.el @@ -569,9 +569,10 @@ dates in the past, and negative for dates in the future." ;; FIXME: `pcase-let*' is easier to use to destructure this, but if I use ;; that, I want to use map 2.1 for the extra convenience, but I can't force ;; that to be installed into the makem.sh sandbox, so I just use `-let*' here. - (-let* ((org-ql-view-plist (car (bookmark-get-bookmark-record bookmark))) + (-let* ((record (bookmark-get-bookmark-record bookmark)) + ((&alist 'org-ql-view-plist) record) ((&plist :buffers-files :query :super-groups :narrow :sort :title) - (cdr org-ql-view-plist)) + org-ql-view-plist) (super-groups (cl-etypecase super-groups (symbol (symbol-value super-groups)) (list super-groups)))) From d73d28f4c3a8f27c985015ef38d171e0295693c5 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Mon, 16 Nov 2020 22:13:23 -0600 Subject: [PATCH 049/372] Notes: Add idea --- notes.org | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/notes.org b/notes.org index 1f87eb7..76c4ddf 100644 --- a/notes.org +++ b/notes.org @@ -89,6 +89,7 @@ - [[#partial-match-for-property-queries][Partial match for property queries]] - [[#org-agenda-skip-function][org-agenda-skip-function]] - [[#update-commentary][Update commentary]] +- [[#org-ql-block-let-org-agenda-format-its-output][org-ql-block: Let org-agenda format its output]] - [[#overlay-based-caching-inspired-by-org-num-mode][Overlay-based caching inspired by org-num-mode]] - [[#alternative-parsing-libraries][Alternative parsing libraries]] - [[#fancier-searching-for-inherited-tags][Fancier searching for inherited tags]] @@ -215,6 +216,10 @@ I should benchmark it to see how much difference it makes, because all those ~fs ** TODO [#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 [[https://github.com/alphapapa/org-ql/pull/113#issuecomment-728674220][#113]]. It might actually be simple to do and might work very well. + ** 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. From 0ca6b4b119992f79104671fe0dc62072ee200ab8 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Mon, 16 Nov 2020 23:51:24 -0600 Subject: [PATCH 050/372] Tidy: Docstring --- org-ql-search.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/org-ql-search.el b/org-ql-search.el index 9f555ce..8a012be 100644 --- a/org-ql-search.el +++ b/org-ql-search.el @@ -243,7 +243,7 @@ automatically from the query." "Insert content for org-ql dynamic block at point according to PARAMS. Valid parameters include: - :query An Org QL query expression in either sexp or non-sexp + :query An Org QL query expression in either sexp or string form. :columns A list of columns, including `heading', `todo', From 71703def0406da8df12f435a752b4e5eb57dd250 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Mon, 16 Nov 2020 23:52:15 -0600 Subject: [PATCH 051/372] Change: Query safety checks - Variable org-ql-ask-unsafe-queries replaces org-ql-view-ask-unsafe-links. - New function: org-ql--ask-unsafe-query. - Tests: Improve --- org-ql-search.el | 4 ++- org-ql-view.el | 33 +++---------------------- org-ql.el | 31 +++++++++++++++++++++++ tests/test-org-ql.el | 58 +++++++++++++++++++++++++++++++++----------- 4 files changed, 82 insertions(+), 44 deletions(-) diff --git a/org-ql-search.el b/org-ql-search.el index 8a012be..145b37c 100644 --- a/org-ql-search.el +++ b/org-ql-search.el @@ -275,7 +275,9 @@ For example, an org-ql dynamic block header could look like: (-let* (((&plist :query :columns :sort :ts-format :take) params) (query (cl-etypecase query (string (org-ql--plain-query query)) - (t query))) + (list ;; SAFETY: Query is in sexp form: ask for confirmation, because it could contain arbitrary code. + (org-ql--ask-unsafe-query query) + query))) (columns (or columns '(heading todo (priority "P")))) ;; MAYBE: Custom column functions. (format-fns diff --git a/org-ql-view.el b/org-ql-view.el index 29e5499..089baf6 100644 --- a/org-ql-view.el +++ b/org-ql-view.el @@ -97,23 +97,6 @@ Based on `org-agenda-mode-map'.") "Options for `org-ql-view'." :group 'org-ql) -(defcustom org-ql-view-ask-unsafe-links t - "Ask before opening a link that could run arbitrary code. -Org QL queries in sexp form can contain arbitrary expressions. -When opening an \"org-ql-search:\" link that contains a query in -sexp form, and this option is non-nil, the user will be prompted -for confirmation before opening the link. - -This variable may be set file-locally to disable this warning in -files that the user assumes are safe (e.g. of known provenance). -Users who are entirely unconcerned about this issue may disable -the option globally (at their own risk, however minimal it -probably is). - -See Info node `(org-ql)Queries'." - :type 'boolean - :risky t) - (defcustom org-ql-view-display-buffer-action nil "Action argument passed through `pop-to-buffer' to `display-buffer', which see." :type '(cons function alist)) @@ -644,18 +627,10 @@ protocol. See, e.g. `org-ql-view--link-store'." (stringp buffers-files) (cl-every #'stringp buffers-files)) (error "CAUTION: Link not opened because unsafe buffers-files parameter detected: %s" buffers-files)) - (when (and org-ql-view-ask-unsafe-links - (or (string-match (rx bol (0+ space) "(") query) - (listp query))) - ;; Query is in sexp form: ask for confirmation, because it could contain arbitrary code. - (let ((query-string (propertize (cl-etypecase query - (list (prin1-to-string query)) - (string query)) - ;; FIXME: The face doesn't seem to be displayed. - 'face 'font-lock-warning-face))) - (unless (yes-or-no-p (concat "Query is in sexp form and could contain arbitrary code: " - query-string " Execute it? ")) - (user-error "Query aborted by user")))) + (when (or (listp query) + (string-match (rx bol (0+ space) "(") query)) + ;; SAFETY: Query is in sexp form: ask for confirmation, because it could contain arbitrary code. + (org-ql--ask-unsafe-query query)) (org-ql-search buffers-files query :sort sort :super-groups groups diff --git a/org-ql.el b/org-ql.el index 3b97307..c479161 100644 --- a/org-ql.el +++ b/org-ql.el @@ -137,6 +137,24 @@ This list should not contain any duplicates.")) ;; TODO: Add info manual link. :link '(url-link "https://github.com/alphapapa/org-ql")) +(defcustom org-ql-ask-unsafe-queries t + "Ask before running a query that could run arbitrary code. +Org QL queries in sexp form can contain arbitrary expressions. +When opening an \"org-ql-search:\" link or updating a dynamic +block that contains a query in sexp form, and this option is +non-nil, the user will be prompted for confirmation before +opening the link. + +This variable may be set file-locally to disable this warning in +files that the user assumes are safe (e.g. of known provenance). +Users who are entirely unconcerned about this issue may disable +the option globally (at their own risk, however minimal it +probably is). + +See Info node `(org-ql)Queries'." + :type 'boolean + :risky t) + ;;;; Macros (cl-defmacro org-ql--defpred (name args docstring &rest body) @@ -531,6 +549,19 @@ from within ELEMENT's buffer." (setf (cadr element) properties) element)) +(defun org-ql--ask-unsafe-query (query) + "Signal an error if user rejects running QUERY. +If `org-ql-view-ask-unsafe-links' is nil, does nothing and +returns nil." + (when org-ql-ask-unsafe-queries + (let ((query-string (propertize (cl-etypecase query + (list (prin1-to-string query)) + (string query)) + 'face 'font-lock-warning-face))) + (unless (yes-or-no-p (concat "Query is in sexp form and could contain arbitrary code: " + query-string " Execute it? ")) + (user-error "Query aborted by user"))))) + ;;;;; Query processing ;; Processing, compiling, etc. for queries. diff --git a/tests/test-org-ql.el b/tests/test-org-ql.el index be81d81..e1c40d5 100644 --- a/tests/test-org-ql.el +++ b/tests/test-org-ql.el @@ -1422,7 +1422,7 @@ RESULTS should be a list of strings as returned by (org-mode))) (cl-flet* ((open-link-in - (link buffer) + (link buffer input) ;; Org REDUCED THE NUMBER OF ARGUMENTS TO `org-open-link-from-string'! That BREAKS BACKWARD ;; COMPATIBILITY! So I have to make my own function so these tests can work across Org versions! (with-current-buffer buffer @@ -1431,9 +1431,12 @@ RESULTS should be a list of strings as returned by (insert "* TODO Test heading\n\n") (insert link) (backward-char 1) - (org-open-at-point))) + (with-simulated-input input + (org-open-at-point)))) + (var-after-link-save-open - (var buffers-files query &key sort super-groups (buffer link-buffer) (input "RET")) + (var buffers-files query &key sort super-groups + (buffer link-buffer) (store-input "RET") open-input) (org-ql-search buffers-files query :super-groups super-groups :sort sort :title title :buffer view-buffer) @@ -1441,26 +1444,53 @@ RESULTS should be a list of strings as returned by (cl-assert (member '("org-ql-search" :follow org-ql-view--link-open :store org-ql-view--link-store) org-link-parameters) t) - (with-simulated-input input + (with-simulated-input store-input ;; Avoid writing "Stored: ..." to test output. (let ((inhibit-message t)) (call-interactively #'org-store-link nil))) (kill-buffer)) (cl-assert (and org-stored-links (caar org-stored-links)) t) - (open-link-in (caar org-stored-links) buffer) + (open-link-in (caar org-stored-links) buffer open-input) (with-current-buffer (get-buffer (concat "*Org QL View: " title "*")) (prog1 (buffer-local-value var (current-buffer)) (kill-buffer))))) (describe "Queries" :var ((string-query "todo:TODO regexp:heading") - (sexp-query '(and (todo "TODO") (regexp "heading")))) - (it "Sexps match" - (expect (var-after-link-save-open 'org-ql-view-query temp-filenames sexp-query) :to-equal sexp-query)) - (it "Strings match" - ;; NOTE: This test includes the string query being replaced with its sexp form after the query is run. - (expect (var-after-link-save-open 'org-ql-view-query temp-filenames string-query) - :to-equal sexp-query))) + (string-query-in-sexp-form '(and (todo "TODO") (regexp "heading"))) + ;; NOTE: The sexp query must be one that `org-ql--query-sexp-to-string' + ;; can't convert to a string. + (sexp-query '(or (todo "TODO") (regexp "heading")))) + + (describe "in sexp form" + + (describe "prompt when `org-ql-ask-unsafe-queries' is non-nil" + :var ((org-ql-ask-unsafe-queries t)) + + (it "and signal an error when rejected by user" + (expect (var-after-link-save-open 'org-ql-view-query temp-filenames sexp-query + :open-input "no RET") + :to-throw 'user-error '("Query aborted by user"))) + (it "and run when approved by user" + (expect (var-after-link-save-open 'org-ql-view-query temp-filenames sexp-query + :open-input "yes RET") + :to-equal sexp-query))) + + (it "don't prompt when `org-ql-ask-unsafe-queries' is nil" + (let ((org-ql-ask-unsafe-queries nil)) + (expect (var-after-link-save-open 'org-ql-view-query temp-filenames sexp-query) + :to-equal sexp-query))) + + (it "match after restoring" + (let ((org-ql-ask-unsafe-queries nil)) ; Disable safety check for this test. + (expect (var-after-link-save-open 'org-ql-view-query temp-filenames sexp-query) + :to-equal sexp-query)))) + + (describe "in string form" + (it "match after restoring" + ;; NOTE: This test includes the string query being replaced with its sexp form after the query is run. + (expect (var-after-link-save-open 'org-ql-view-query temp-filenames string-query) + :to-equal string-query-in-sexp-form)))) (describe "Grouping" :var ((query '(and (todo "TODO") (regexp "heading"))) @@ -1487,11 +1517,11 @@ RESULTS should be a list of strings as returned by (one-filename (car temp-filenames))) (it "Can search a file by filename" (expect (var-after-link-save-open 'org-ql-view-buffers-files one-filename query - :input "M-n M-n RET") + :store-input "M-n M-n RET") :to-equal one-filename)) (it "Can search multiple files by filename" (expect (var-after-link-save-open 'org-ql-view-buffers-files temp-filenames query - :input "M-n M-n RET") + :store-input "M-n M-n RET") :to-equal temp-filenames)) (it "Can search buffer containing the link" ;; This is sort-of a special case because of how the test link-opening function works. From a02514c0dc8dfef1e8425a93cd23d91e72fbc1d5 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Tue, 17 Nov 2020 00:24:26 -0600 Subject: [PATCH 052/372] Tests: Test dynamic block sexp query warning --- tests/test-org-ql.el | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/test-org-ql.el b/tests/test-org-ql.el index e1c40d5..7dad757 100644 --- a/tests/test-org-ql.el +++ b/tests/test-org-ql.el @@ -1406,6 +1406,34 @@ RESULTS should be a list of strings as returned by (mapcar #'find-file-noselect temp-filenames) query) :to-equal temp-filenames))))) + (describe "Dynamic blocks" + (describe "warn about sexp queries" + + (cl-flet ((test-dblock + (&optional input) (with-current-buffer (get-buffer-create "*TEST DBLOCK*") + (erase-buffer) + (org-mode) + (insert "* TODO Heading 1\n\n" + "#+BEGIN: org-ql :query (or (todo) (regexp \"Heading\")) :columns (todo)\n" + "#+END:") + (goto-char (point-min)) + (forward-line 2) + (with-simulated-input input + (org-dblock-update)) + (kill-buffer)))) + + (it "when org-ql-ask-unsafe-queries is non-nil" + ;; TODO: Should the query be converted to string form if possible and only warn if not? + (let ((org-ql-ask-unsafe-queries t)) + (expect (test-dblock "no RET") + :to-throw 'user-error '("Query aborted by user")))) + + (it "unless org-ql-ask-unsafe-queries is nil" + ;; TODO: Should the query be converted to string form if possible and only warn if not? + (let ((org-ql-ask-unsafe-queries nil)) + (expect (test-dblock) + :not :to-throw)))))) + (describe "Links" ;; Not sure if this binding works. :var ((title "ORG-QL-TEST") From 814f3f0edaf2cfd4a29d3ea98a6dc767e9651217 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Tue, 17 Nov 2020 00:25:34 -0600 Subject: [PATCH 053/372] Notes: Update --- notes.org | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/notes.org b/notes.org index 76c4ddf..6dfba38 100644 --- a/notes.org +++ b/notes.org @@ -40,7 +40,6 @@ #+BEGIN: org-ql :query "todo: priority:A" :columns ((priority "P") ((property "milestone") "M") todo heading) :sort (priority date) :take 7 | P | M | Todo | Heading | |---+-----+---------+--------------------------------------------------------------| -| A | 0.5 | NEXT | [[Make dynamic blocks warn about sexp queries][Make dynamic blocks warn about sexp queries]] | | A | 0.5 | PROJECT | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | | A | 0.5 | PROJECT | [[Convert simple sexp queries to non-sexp][Convert simple sexp queries to non-sexp]] | | A | 0.5 | PROJECT | [[Checking links for unsafe parameters][Checking links for unsafe parameters]] | @@ -65,15 +64,15 @@ ** Milestones ([[org-ql-search:todo:?super-groups=((:auto-property%20"milestone"))&sort=(todo)&title="Milestones"][view]]) #+BEGIN: org-ql :query "todo: property:milestone" :columns (((property "milestone") "M") (priority "P") todo heading) :sort (priority date) :take 7 -| M | P | Todo | Heading | -|-----+---+---------+--------------------------------------------------------------| -| 0.5 | A | NEXT | [[Make dynamic blocks warn about sexp queries][Make dynamic blocks warn about sexp queries]] | -| 0.5 | A | PROJECT | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | -| 0.5 | A | PROJECT | [[Convert simple sexp queries to non-sexp][Convert simple sexp queries to non-sexp]] | -| 0.5 | A | PROJECT | [[Checking links for unsafe parameters][Checking links for unsafe parameters]] | -| 0.5 | A | TODO | [[Tag org-super-agenda 1.2 and bump required version in org-ql][Tag org-super-agenda 1.2 and bump required version in org-ql]] | -| 0.6 | B | PROJECT | [[Group tag support][Group tag support]] | -| 0.6 | B | PROJECT | [[Save view to dynamic block][Save view to dynamic block]] | +| M | P | Todo | Heading | +|--------+---+---------+--------------------------------------------------------------| +| 0.5 | A | PROJECT | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | +| 0.5 | A | PROJECT | [[Convert simple sexp queries to non-sexp][Convert simple sexp queries to non-sexp]] | +| 0.5 | A | PROJECT | [[Checking links for unsafe parameters][Checking links for unsafe parameters]] | +| 0.5 | A | TODO | [[Tag org-super-agenda 1.2 and bump required version in org-ql][Tag org-super-agenda 1.2 and bump required version in org-ql]] | +| 0.6 | B | PROJECT | [[Group tag support][Group tag support]] | +| 0.6 | B | PROJECT | [[Save view to dynamic block][Save view to dynamic block]] | +| future | C | MAYBE | [[Alternative parsing libraries][Alternative parsing libraries]] | #+END: * [#A] Tasks @@ -93,7 +92,6 @@ - [[#overlay-based-caching-inspired-by-org-num-mode][Overlay-based caching inspired by org-num-mode]] - [[#alternative-parsing-libraries][Alternative parsing libraries]] - [[#fancier-searching-for-inherited-tags][Fancier searching for inherited tags]] -- [[#make-dynamic-blocks-warn-about-sexp-queries][Make dynamic blocks warn about sexp queries]] - [[#compatibility-with-org-94-custom-link-changes][Compatibility with Org 9.4 custom link changes]] - [[#convert-simple-sexp-queries-to-non-sexp][Convert simple sexp queries to non-sexp]] - [[#checking-links-for-unsafe-parameters][Checking links for unsafe parameters]] @@ -111,6 +109,7 @@ - [[#update-view-screenshots][Update view screenshots]] - [[#test-caching][Test caching]] - [[#node-caching]["Node" caching]] +- [[#make-dynamic-blocks-warn-about-sexp-queries][Make dynamic blocks warn about sexp queries]] - [[#add-emacs-271-to-testyml][Add Emacs 27.1 to test.yml]] - [[#fix-org-ql-view--link-open-on-org-93][Fix org-ql-view--link-open on Org 9.3+]] - [[#fix-query-sexp-to-string-functions-handling-of-eg-descendants][Fix query-sexp-to-string function's handling of, e.g. descendants]] @@ -240,13 +239,6 @@ However, there might still be a useful idea here somewhere... 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. -** NEXT [#A] Make dynamic blocks warn about sexp queries -:PROPERTIES: -:milestone: 0.5 -:END: - -[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. - ** PROJECT [#A] Compatibility with Org 9.4 custom link changes :bug: :PROPERTIES: :milestone: 0.5 @@ -1419,6 +1411,18 @@ This works okay (except the priority accessor needs to be fixed, because Org pri :action #'helm-org-goto-marker)))) #+END_SRC +** DONE [#A] Make dynamic blocks warn about sexp queries +:PROPERTIES: +:milestone: 0.5 +:END: +:LOGBOOK: +CLOCK: [2020-11-16 Mon 22:56]--[2020-11-17 Tue 00:25] => 1:29 +:END: + +[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. + ** DONE [#A] Add Emacs 27.1 to =test.yml= :PROPERTIES: :milestone: 0.5 From 619fd8c403da93bc204bd0ae58ddd52c7678cda5 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Tue, 17 Nov 2020 00:35:53 -0600 Subject: [PATCH 054/372] Notes: Update --- notes.org | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/notes.org b/notes.org index 6dfba38..74f0331 100644 --- a/notes.org +++ b/notes.org @@ -23,12 +23,13 @@ #+BEGIN: org-ql :query "todo: tags:bug" :columns ((priority "P") ((property "milestone") "M") (todo "Keyword") heading) :sort (priority todo date) :ts-format "%Y-%m-%d %H:%M" | P | M | Keyword | Heading | |---+-----+---------+------------------------------------------------| +| A | 0.5 | TODO | [[Views: Multiple sorters are not preserved][Views: Multiple sorters are not preserved]] | | A | 0.5 | PROJECT | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | #+END: ** Underway ([[org-ql-search:todo%253AUNDERWAY?sort=%2528priority%2529&title=%2522Underway%2522][view]]) -#+BEGIN: org-ql :query (todo "UNDERWAY") :columns (((property "milestone") "M") (priority "P") (todo "Keyword") heading) :sort (priority date) :ts-format "%Y-%m-%d %H:%M" +#+BEGIN: org-ql :query "todo:UNDERWAY" :columns (((property "milestone") "M") (priority "P") (todo "Keyword") heading) :sort (priority date) :ts-format "%Y-%m-%d %H:%M" | M | P | Keyword | Heading | |---+---+----------+--------------------------------------------------------------------| | | B | UNDERWAY | [["Node" caching]["Node" caching]] | @@ -40,6 +41,7 @@ #+BEGIN: org-ql :query "todo: priority:A" :columns ((priority "P") ((property "milestone") "M") todo heading) :sort (priority date) :take 7 | P | M | Todo | Heading | |---+-----+---------+--------------------------------------------------------------| +| A | 0.5 | TODO | [[Views: Multiple sorters are not preserved][Views: Multiple sorters are not preserved]] | | A | 0.5 | PROJECT | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | | A | 0.5 | PROJECT | [[Convert simple sexp queries to non-sexp][Convert simple sexp queries to non-sexp]] | | A | 0.5 | PROJECT | [[Checking links for unsafe parameters][Checking links for unsafe parameters]] | @@ -61,18 +63,18 @@ | 0.6 | B | [[Group tag support][Group tag support]] | #+END: -** Milestones ([[org-ql-search:todo:?super-groups=((:auto-property%20"milestone"))&sort=(todo)&title="Milestones"][view]]) +** Milestones ([[org-ql-search:todo%253A?super-groups=%2528%2528%253Aauto-property%2520%2522milestone%2522%2529%2529&sort=%2528todo%2529&title=%2522Milestones%2522][view]]) #+BEGIN: org-ql :query "todo: property:milestone" :columns (((property "milestone") "M") (priority "P") todo heading) :sort (priority date) :take 7 -| M | P | Todo | Heading | -|--------+---+---------+--------------------------------------------------------------| -| 0.5 | A | PROJECT | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | -| 0.5 | A | PROJECT | [[Convert simple sexp queries to non-sexp][Convert simple sexp queries to non-sexp]] | -| 0.5 | A | PROJECT | [[Checking links for unsafe parameters][Checking links for unsafe parameters]] | -| 0.5 | A | TODO | [[Tag org-super-agenda 1.2 and bump required version in org-ql][Tag org-super-agenda 1.2 and bump required version in org-ql]] | -| 0.6 | B | PROJECT | [[Group tag support][Group tag support]] | -| 0.6 | B | PROJECT | [[Save view to dynamic block][Save view to dynamic block]] | -| future | C | MAYBE | [[Alternative parsing libraries][Alternative parsing libraries]] | +| M | P | Todo | Heading | +|-----+---+---------+--------------------------------------------------------------| +| 0.5 | A | TODO | [[Views: Multiple sorters are not preserved][Views: Multiple sorters are not preserved]] | +| 0.5 | A | PROJECT | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | +| 0.5 | A | PROJECT | [[Convert simple sexp queries to non-sexp][Convert simple sexp queries to non-sexp]] | +| 0.5 | A | PROJECT | [[Checking links for unsafe parameters][Checking links for unsafe parameters]] | +| 0.5 | A | TODO | [[Tag org-super-agenda 1.2 and bump required version in org-ql][Tag org-super-agenda 1.2 and bump required version in org-ql]] | +| 0.6 | B | PROJECT | [[Group tag support][Group tag support]] | +| 0.6 | B | PROJECT | [[Save view to dynamic block][Save view to dynamic block]] | #+END: * [#A] Tasks @@ -80,6 +82,7 @@ :TOC: :include descendants :depth 1 :END: :CONTENTS: +- [[#views-multiple-sorters-are-not-preserved][Views: Multiple sorters are not preserved]] - [[#add-auto-keyword-to-planning-predicate][Add :auto keyword to (planning) predicate]] - [[#change-deadlines-auto-argument-to-auto-andor-auto-t][Change (deadline)'s auto argument to :auto and/or :auto t]] - [[#outline-path-in-buffers-files-arg][Outline path in buffers-files arg]] @@ -122,6 +125,12 @@ - [[#use-macros-for-date][Use macros for date]] :END: +** TODO [#A] Views: Multiple sorters are not preserved :bug: +:PROPERTIES: +:milestone: 0.5 +:url: https://github.com/alphapapa/org-ql/issues/136 +:END: + ** TODO [#B] Add ~:auto~ keyword to ~(planning)~ predicate It should act like ~(or (deadline auto) (scheduled :to today))~. @@ -4235,5 +4244,6 @@ I don't need this right now, but it might come in handy here or elsewhere. # Local Variables: # before-save-hook: ((lambda () (when (fboundp 'unpackaged/org-fix-blank-lines) (unpackaged/org-fix-blank-lines t))) (lambda () (when (fboundp 'ap/org-sort-entries-recursive-multi) (save-excursion (goto-char (point-min)) (ap/org-sort-entries-recursive-multi '(?a ?p ?o))))) org-update-all-dblocks org-make-toc) +# org-ql-ask-unsafe-queries: nil # End: From d500d28af53b479de23140908b68658475758c14 Mon Sep 17 00:00:00 2001 From: Imran Khan Date: Sun, 23 Aug 2020 00:20:16 +0600 Subject: [PATCH 055/372] Change nreverse to non-destructive version --- org-ql.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/org-ql.el b/org-ql.el index f7efbbf..a2d3198 100644 --- a/org-ql.el +++ b/org-ql.el @@ -1456,7 +1456,7 @@ PREDICATES is a list of one or more sorting methods, including: ;; Put at end of list if not found (1+ (length org-todo-keywords-1))))))) (-flatten-n 1 (-map #'cdr sorted-groups))))) - (cl-loop for pred in (nreverse predicates) + (cl-loop for pred in (reverse predicates) do (setq items (if (eq pred 'todo) (sort-by-todo-keyword items) (-sort (sorter pred) items))) From 6618318622c0b49dc0c22eb21dd74a33b425e574 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Wed, 18 Nov 2020 00:03:41 -0600 Subject: [PATCH 056/372] Update readme, version header --- README.org | 5 +++++ org-ql.el | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.org b/README.org index 23c4a9d..9b6b5d7 100644 --- a/README.org +++ b/README.org @@ -398,6 +398,11 @@ Expands into a call to ~org-ql-select~ with the same arguments. For convenience /Note:/ Breaking changes may be made before version 1.0, but in the event of major changes, attempts at backward compatibility will be made with obsolescence declarations, translation of arguments, etc. Users who need stability guarantees before 1.0 may choose to use tagged stable releases. +** 0.4.8 + +*Fixed* ++ Multiple sorters not preserved when refreshing views. (Fixes [[https://github.com/alphapapa/org-ql/issues/136][#136]], [[https://github.com/alphapapa/org-ql/pull/137][#137]]. Thanks to [[https://github.com/natrys][Imran Khan]].) + ** 0.4.7 *Fixed* diff --git a/org-ql.el b/org-ql.el index a2d3198..85b47cd 100644 --- a/org-ql.el +++ b/org-ql.el @@ -2,7 +2,7 @@ ;; Author: Adam Porter ;; Url: https://github.com/alphapapa/org-ql -;; Version: 0.4.6 +;; Version: 0.4.8 ;; Package-Requires: ((emacs "26.1") (dash "2.13") (dash-functional "1.2.0") (f "0.17.2") (org "9.0") (org-super-agenda "1.2-pre") (ov "1.0.6") (peg "0.6") (s "1.12.0") (ts "0.2-pre")) ;; Keywords: hypermedia, outlines, Org, agenda From a9c734506677b66b544a4aa4e31cac5050a20134 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Wed, 18 Nov 2020 03:04:34 -0600 Subject: [PATCH 057/372] Notes: Add idea --- notes.org | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/notes.org b/notes.org index 74f0331..a04c989 100644 --- a/notes.org +++ b/notes.org @@ -92,6 +92,7 @@ - [[#org-agenda-skip-function][org-agenda-skip-function]] - [[#update-commentary][Update commentary]] - [[#org-ql-block-let-org-agenda-format-its-output][org-ql-block: Let org-agenda format its output]] +- [[#omnifocus-like-screencasts][OmniFocus-like screencasts]] - [[#overlay-based-caching-inspired-by-org-num-mode][Overlay-based caching inspired by org-num-mode]] - [[#alternative-parsing-libraries][Alternative parsing libraries]] - [[#fancier-searching-for-inherited-tags][Fancier searching for inherited tags]] @@ -228,6 +229,10 @@ I should benchmark it to see how much difference it makes, because all those ~fs [2020-11-16 Mon 22:12] As suggested by Kevin J. Foley. See [[https://github.com/alphapapa/org-ql/pull/113#issuecomment-728674220][#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 [#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. From 6b96c3684ded8650981b99f3a0427c13d3dba07a Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Wed, 18 Nov 2020 04:16:07 -0600 Subject: [PATCH 058/372] Notes: Update --- notes.org | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/notes.org b/notes.org index a04c989..95dc303 100644 --- a/notes.org +++ b/notes.org @@ -23,7 +23,6 @@ #+BEGIN: org-ql :query "todo: tags:bug" :columns ((priority "P") ((property "milestone") "M") (todo "Keyword") heading) :sort (priority todo date) :ts-format "%Y-%m-%d %H:%M" | P | M | Keyword | Heading | |---+-----+---------+------------------------------------------------| -| A | 0.5 | TODO | [[Views: Multiple sorters are not preserved][Views: Multiple sorters are not preserved]] | | A | 0.5 | PROJECT | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | #+END: @@ -41,7 +40,6 @@ #+BEGIN: org-ql :query "todo: priority:A" :columns ((priority "P") ((property "milestone") "M") todo heading) :sort (priority date) :take 7 | P | M | Todo | Heading | |---+-----+---------+--------------------------------------------------------------| -| A | 0.5 | TODO | [[Views: Multiple sorters are not preserved][Views: Multiple sorters are not preserved]] | | A | 0.5 | PROJECT | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | | A | 0.5 | PROJECT | [[Convert simple sexp queries to non-sexp][Convert simple sexp queries to non-sexp]] | | A | 0.5 | PROJECT | [[Checking links for unsafe parameters][Checking links for unsafe parameters]] | @@ -66,15 +64,15 @@ ** Milestones ([[org-ql-search:todo%253A?super-groups=%2528%2528%253Aauto-property%2520%2522milestone%2522%2529%2529&sort=%2528todo%2529&title=%2522Milestones%2522][view]]) #+BEGIN: org-ql :query "todo: property:milestone" :columns (((property "milestone") "M") (priority "P") todo heading) :sort (priority date) :take 7 -| M | P | Todo | Heading | -|-----+---+---------+--------------------------------------------------------------| -| 0.5 | A | TODO | [[Views: Multiple sorters are not preserved][Views: Multiple sorters are not preserved]] | -| 0.5 | A | PROJECT | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | -| 0.5 | A | PROJECT | [[Convert simple sexp queries to non-sexp][Convert simple sexp queries to non-sexp]] | -| 0.5 | A | PROJECT | [[Checking links for unsafe parameters][Checking links for unsafe parameters]] | -| 0.5 | A | TODO | [[Tag org-super-agenda 1.2 and bump required version in org-ql][Tag org-super-agenda 1.2 and bump required version in org-ql]] | -| 0.6 | B | PROJECT | [[Group tag support][Group tag support]] | -| 0.6 | B | PROJECT | [[Save view to dynamic block][Save view to dynamic block]] | +| M | P | Todo | Heading | +|--------+---+---------+--------------------------------------------------------------| +| 0.5 | A | PROJECT | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | +| 0.5 | A | PROJECT | [[Convert simple sexp queries to non-sexp][Convert simple sexp queries to non-sexp]] | +| 0.5 | A | PROJECT | [[Checking links for unsafe parameters][Checking links for unsafe parameters]] | +| 0.5 | A | TODO | [[Tag org-super-agenda 1.2 and bump required version in org-ql][Tag org-super-agenda 1.2 and bump required version in org-ql]] | +| 0.6 | B | PROJECT | [[Group tag support][Group tag support]] | +| 0.6 | B | PROJECT | [[Save view to dynamic block][Save view to dynamic block]] | +| future | C | MAYBE | [[Alternative parsing libraries][Alternative parsing libraries]] | #+END: * [#A] Tasks @@ -82,7 +80,6 @@ :TOC: :include descendants :depth 1 :END: :CONTENTS: -- [[#views-multiple-sorters-are-not-preserved][Views: Multiple sorters are not preserved]] - [[#add-auto-keyword-to-planning-predicate][Add :auto keyword to (planning) predicate]] - [[#change-deadlines-auto-argument-to-auto-andor-auto-t][Change (deadline)'s auto argument to :auto and/or :auto t]] - [[#outline-path-in-buffers-files-arg][Outline path in buffers-files arg]] @@ -113,6 +110,7 @@ - [[#update-view-screenshots][Update view screenshots]] - [[#test-caching][Test caching]] - [[#node-caching]["Node" caching]] +- [[#views-multiple-sorters-are-not-preserved][Views: Multiple sorters are not preserved]] - [[#make-dynamic-blocks-warn-about-sexp-queries][Make dynamic blocks warn about sexp queries]] - [[#add-emacs-271-to-testyml][Add Emacs 27.1 to test.yml]] - [[#fix-org-ql-view--link-open-on-org-93][Fix org-ql-view--link-open on Org 9.3+]] @@ -126,12 +124,6 @@ - [[#use-macros-for-date][Use macros for date]] :END: -** TODO [#A] Views: Multiple sorters are not preserved :bug: -:PROPERTIES: -:milestone: 0.5 -:url: https://github.com/alphapapa/org-ql/issues/136 -:END: - ** TODO [#B] Add ~:auto~ keyword to ~(planning)~ predicate It should act like ~(or (deadline auto) (scheduled :to today))~. @@ -1425,6 +1417,12 @@ This works okay (except the priority accessor needs to be fixed, because Org pri :action #'helm-org-goto-marker)))) #+END_SRC +** DONE [#A] Views: Multiple sorters are not preserved :bug: +:PROPERTIES: +:milestone: 0.5 +:url: https://github.com/alphapapa/org-ql/issues/136 +:END: + ** DONE [#A] Make dynamic blocks warn about sexp queries :PROPERTIES: :milestone: 0.5 From e2dbeb462098bc70cae116a1a5ba78e20575c870 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Wed, 18 Nov 2020 04:31:15 -0600 Subject: [PATCH 059/372] Notes: Move to meta/notes branch --- .gitignore | 2 + NOTES | 1 + notes.org | 4252 ---------------------------------------------------- 3 files changed, 3 insertions(+), 4252 deletions(-) create mode 100644 NOTES delete mode 100644 notes.org diff --git a/.gitignore b/.gitignore index fa4f2fc..9355658 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ /scratch.org /scratch/ /worktrees/ +/worktree/ +/NOTES.org diff --git a/NOTES b/NOTES new file mode 100644 index 0000000..b869aeb --- /dev/null +++ b/NOTES @@ -0,0 +1 @@ +Note: Notes are kept in the file "NOTES.org" in the "meta/notes" branch. diff --git a/notes.org b/notes.org deleted file mode 100644 index 95dc303..0000000 --- a/notes.org +++ /dev/null @@ -1,4252 +0,0 @@ -#+PROPERTY: LOGGING nil -#+TODO: TODO MAYBE NEXT PROJECT UNDERWAY WAITING | DONE(d) CANCELED - -* [#A] Contents -:PROPERTIES: -:TOC: :include siblings :depth 1 :ignore this :force depth -:END: -:CONTENTS: -- [[#overview][Overview]] -- [[#tasks][Tasks]] -- [[#checklists][Checklists]] -- [[#examples--testing][Examples / testing]] -- [[#in-the-wild][In the wild]] -- [[#profiling][Profiling]] -- [[#references][References]] -- [[#testing][Testing]] -:END: - -* [#A] Overview - -** Bugs ([[org-ql-search:todo%253A%2520tags%253Abug?super-groups=%2528%2528%253Aauto-property%2520%2522Milestone%2522%2529%2529&sort=%2528todo%2529&title=%2522Bugs%2522][view]]) - -#+BEGIN: org-ql :query "todo: tags:bug" :columns ((priority "P") ((property "milestone") "M") (todo "Keyword") heading) :sort (priority todo date) :ts-format "%Y-%m-%d %H:%M" -| P | M | Keyword | Heading | -|---+-----+---------+------------------------------------------------| -| A | 0.5 | PROJECT | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | -#+END: - -** Underway ([[org-ql-search:todo%253AUNDERWAY?sort=%2528priority%2529&title=%2522Underway%2522][view]]) - -#+BEGIN: org-ql :query "todo:UNDERWAY" :columns (((property "milestone") "M") (priority "P") (todo "Keyword") heading) :sort (priority date) :ts-format "%Y-%m-%d %H:%M" -| M | P | Keyword | Heading | -|---+---+----------+--------------------------------------------------------------------| -| | B | UNDERWAY | [["Node" caching]["Node" caching]] | -| | | UNDERWAY | [[Benchmarking tags searches without and with new group-tags support][Benchmarking tags searches without and with new group-tags support]] | -#+END: - -** To-do ([[org-ql-search:todo:?super-groups=((:todo%20"NEXT")%20(:todo%20"PROJECT")%20(:auto-priority))&sort=(todo)][view]]) - -#+BEGIN: org-ql :query "todo: priority:A" :columns ((priority "P") ((property "milestone") "M") todo heading) :sort (priority date) :take 7 -| P | M | Todo | Heading | -|---+-----+---------+--------------------------------------------------------------| -| A | 0.5 | PROJECT | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | -| A | 0.5 | PROJECT | [[Convert simple sexp queries to non-sexp][Convert simple sexp queries to non-sexp]] | -| A | 0.5 | PROJECT | [[Checking links for unsafe parameters][Checking links for unsafe parameters]] | -| A | 0.5 | TODO | [[Tag org-super-agenda 1.2 and bump required version in org-ql][Tag org-super-agenda 1.2 and bump required version in org-ql]] | -| A | | PROJECT | [[Org%20link%20types%20%5B2/3%5D][Org link types {2/3}]] | -#+END: - -** Stuck projects ([[org-ql-search:%2528and%2520%2528todo%2520%2522PROJECT%2522%2529%2520%2528not%2520%2528descendants%2520%2528todo%2520%2522NEXT%2522%2529%2529%2529%2529?super-groups=%2528%2528%253Aauto-property%2520%2522milestone%2522%2529%2529&sort=%2528priority%2529&title=%2522Stuck%2520Projects%2522][view]]) - -#+BEGIN: org-ql :query (and (todo "PROJECT") (not (descendants (todo "NEXT")))) :columns (((property "milestone") "M") (priority "P") heading) :sort (priority date) :take 7 -| M | P | Heading | -|-----+---+-------------------------------------------------------| -| 0.5 | A | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | -| 0.5 | A | [[Convert simple sexp queries to non-sexp][Convert simple sexp queries to non-sexp]] | -| 0.5 | A | [[Checking links for unsafe parameters][Checking links for unsafe parameters]] | -| | A | [[Org%20link%20types%20%5B2/3%5D][Org link types {2/3}]] | -| | B | [[Move this notes file into an orphan =meta/notes= branch][Move this notes file into an orphan =meta/notes= branch]] | -| | B | [[Outline path predicate][Outline path predicate]] | -| 0.6 | B | [[Group tag support][Group tag support]] | -#+END: - -** Milestones ([[org-ql-search:todo%253A?super-groups=%2528%2528%253Aauto-property%2520%2522milestone%2522%2529%2529&sort=%2528todo%2529&title=%2522Milestones%2522][view]]) - -#+BEGIN: org-ql :query "todo: property:milestone" :columns (((property "milestone") "M") (priority "P") todo heading) :sort (priority date) :take 7 -| M | P | Todo | Heading | -|--------+---+---------+--------------------------------------------------------------| -| 0.5 | A | PROJECT | [[Compatibility with Org 9.4 custom link changes][Compatibility with Org 9.4 custom link changes]] | -| 0.5 | A | PROJECT | [[Convert simple sexp queries to non-sexp][Convert simple sexp queries to non-sexp]] | -| 0.5 | A | PROJECT | [[Checking links for unsafe parameters][Checking links for unsafe parameters]] | -| 0.5 | A | TODO | [[Tag org-super-agenda 1.2 and bump required version in org-ql][Tag org-super-agenda 1.2 and bump required version in org-ql]] | -| 0.6 | B | PROJECT | [[Group tag support][Group tag support]] | -| 0.6 | B | PROJECT | [[Save view to dynamic block][Save view to dynamic block]] | -| future | C | MAYBE | [[Alternative parsing libraries][Alternative parsing libraries]] | -#+END: - -* [#A] Tasks -:PROPERTIES: -:TOC: :include descendants :depth 1 -:END: -:CONTENTS: -- [[#add-auto-keyword-to-planning-predicate][Add :auto keyword to (planning) predicate]] -- [[#change-deadlines-auto-argument-to-auto-andor-auto-t][Change (deadline)'s auto argument to :auto and/or :auto t]] -- [[#outline-path-in-buffers-files-arg][Outline path in buffers-files arg]] -- [[#add-more-sorters][Add more sorters?]] -- [[#default-sort][Default sort]] -- [[#partial-match-for-property-queries][Partial match for property queries]] -- [[#org-agenda-skip-function][org-agenda-skip-function]] -- [[#update-commentary][Update commentary]] -- [[#org-ql-block-let-org-agenda-format-its-output][org-ql-block: Let org-agenda format its output]] -- [[#omnifocus-like-screencasts][OmniFocus-like screencasts]] -- [[#overlay-based-caching-inspired-by-org-num-mode][Overlay-based caching inspired by org-num-mode]] -- [[#alternative-parsing-libraries][Alternative parsing libraries]] -- [[#fancier-searching-for-inherited-tags][Fancier searching for inherited tags]] -- [[#compatibility-with-org-94-custom-link-changes][Compatibility with Org 9.4 custom link changes]] -- [[#convert-simple-sexp-queries-to-non-sexp][Convert simple sexp queries to non-sexp]] -- [[#checking-links-for-unsafe-parameters][Checking links for unsafe parameters]] -- [[#move-this-notes-file-into-an-orphan-metanotes-branch][Move this notes file into an orphan meta/notes branch]] -- [[#outline-path-predicate][Outline path predicate]] -- [[#tools-for-saving-queries-and-accessing-them-34][Tools for saving queries and accessing them {3/4}]] -- [[#group-tag-support][Group tag support]] -- [[#document-the-sorting-functions][Document the sorting functions]] -- [[#recursive-queries][Recursive queries]] -- [[#timeline-view][Timeline view]] -- [[#implement-view-with-tabulated-list-mode-or-magit-section][Implement view with tabulated-list-mode or magit-section]] -- [[#dynamic-blocks][Dynamic blocks]] -- [[#new-transient-transient-lisp-variable-class][New Transient transient-lisp-variable class]] -- [[#normalize-queries][Normalize queries]] -- [[#update-view-screenshots][Update view screenshots]] -- [[#test-caching][Test caching]] -- [[#node-caching]["Node" caching]] -- [[#views-multiple-sorters-are-not-preserved][Views: Multiple sorters are not preserved]] -- [[#make-dynamic-blocks-warn-about-sexp-queries][Make dynamic blocks warn about sexp queries]] -- [[#add-emacs-271-to-testyml][Add Emacs 27.1 to test.yml]] -- [[#fix-org-ql-view--link-open-on-org-93][Fix org-ql-view--link-open on Org 9.3+]] -- [[#fix-query-sexp-to-string-functions-handling-of-eg-descendants][Fix query-sexp-to-string function's handling of, e.g. descendants]] -- [[#helm-command][Helm command]] -- [[#quickly-change-sortinggrouping-in-search-views][Quickly change sorting/grouping in search views]] -- [[#byte-compile-lambdas][Byte-compile lambdas]] -- [[#documentfigure-out-tag-inheritance][Document/figure out tag inheritance]] -- [[#dual-matching-with-regexp-and-predicates][Dual matching with regexp and predicates]] -- [[#operate-on-list-of-heading-positions][Operate on list of heading positions]] -- [[#use-macros-for-date][Use macros for date]] -:END: - -** TODO [#B] Add ~:auto~ keyword to ~(planning)~ predicate - -It should act like ~(or (deadline auto) (scheduled :to today))~. - -** TODO [#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. - - -** TODO [#B] Outline path in buffers-files arg -:PROPERTIES: -:ID: 6935361a-9e1d-48ec-8d17-876a90b90f50 -:END: - -e.g. - -#+BEGIN_SRC elisp - (org-ql (olp "~/org/inbox.org" "Emacs" "Ideas") - (todo "NEXT")) -#+END_SRC - -Also, should support an ~id~ one. - -** TODO [#B] Add more sorters? - -+ [ ] =category= -+ [ ] Any date :: e.g. it would search for timestamps (active/inactive?) anywhere in an entry - -** TODO [#B] Default sort - -Would probably be useful to have a default sort option. - -** TODO [#B] Partial match for property queries - -e.g. something like [[https://200ok.ch/posts/2020-02-09_creating_org_mode_sparse_trees_in_emacs_and_organice.html][Organice has now]]. - -[2020-02-13 Thu 00:42] Something is needed to help search property values by partial matches. For example: - -#+BEGIN_SRC org - ,* [[https://github.com/fniessen/org-html-themes][org-html-themes: Framework including two themes, Bigblow and ReadTheOrg]] - :PROPERTIES: - :author: Fabrice Niessen - :END: - -#+END_SRC - -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. - -** TODO [#C] ~org-agenda-skip-function~ - -As discussed [[https://www.reddit.com/r/emacs/comments/cnrt2d/orgqlblock_integrates_orgql_into_org_agenda/ewi1q36/][here]], this is a cool feature that allows further integration into existing custom agenda commands. Example: - -#+BEGIN_SRC elisp - ;;; 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")) -#+END_SRC - -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... - -** TODO [#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 [[https://github.com/alphapapa/org-ql/pull/113#issuecomment-728674220][#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 [#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] Alternative parsing libraries -:PROPERTIES: -:milestone: future -:END: - -+ e.g. Bovine and Wisent come with Emacs, which would allow us to drop the =peg= dependency (which doesn't use lexical binding). -+ [[https://github.com/cute-jumper/parsec.el][parsec]] is third-party, but looks powerful - -** 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 :bug: -:PROPERTIES: -:milestone: 0.5 -:END: - -[2020-11-13 Fri 22:36] From [[https://www.orgmode.org/Changes.html][the changelog]]: - -#+BEGIN_QUOTE -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. -#+END_QUOTE - -Unfortunately it does not say what the new, required second argument is. - - -** PROJECT [#A] Convert simple sexp queries to non-sexp -:PROPERTIES: -:milestone: 0.5 -:END: - -[2020-11-11 Wed 00:28] This will be very helpful for storing links. Surely simple ones won't be too hard... - -#+BEGIN_SRC elisp - (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"))) - - - ) -#+END_SRC - -[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 [[https://github.com/alphapapa/org-ql/issues/147#issuecomment-725287074][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] Checking links for unsafe parameters -:PROPERTIES: -:ID: ba70e375-eddb-40df-8892-fb418c1f70d1 -:milestone: 0.5 -:END: -:LOGBOOK: -- State "PROJECT" from "UNDERWAY" [2020-11-12 Thu 00:26] -- State "UNDERWAY" from [2020-11-11 Wed 23:09] -:END: - -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: - -1. For sexp-based queries in links and dynamic blocks, prompt for confirmation before running. -2. 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 ~and~ or something. A whitelist approach might be needed.) - -*** TODO [#A] Tag org-super-agenda 1.2 and bump required version in org-ql -:PROPERTIES: -:milestone: 0.5 -:END: - -That /should/ force the version of org-super-agenda with the fix to be installed when org-ql is upgraded. - -*** DONE Add automated tests -:LOGBOOK: -- 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] -:END: - -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. - -*** DONE Enumerate and test parameters and potentially unsafe types -CLOSED: [2020-11-11 Wed 23:26] -:LOGBOOK: -- State "DONE" from "UNDERWAY" [2020-11-11 Wed 23:26] -- State "UNDERWAY" from [2020-11-11 Wed 23:15] -:END: - -#+CAPTION: Template for making testable links -#+BEGIN_SRC org -[[org-ql-search:todo:?]] -#+END_SRC - -#+CAPTION: Expression to insert encoded values into template (after the =?=) -#+BEGIN_SRC elisp - (insert (url-hexify-string (concat "buffers-files=" (prin1-to-string '((lambda () (message "AHA"))))))) -#+END_SRC - -+ [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] ~:pred~ selector (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-map= selector (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: - -1. 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: - - #+BEGIN_SRC elisp - (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")) - #+END_SRC - -2. Sexp queries already prompt for confirmation, unless the user has set =org-ql-view-ask-unsafe-links= to nil. - -[2020-11-11 Wed 23:27] That's all the parameters and all the types that I can think to test. - - -** PROJECT [#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. - -** 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: - -#+BEGIN_SRC org - ,* Food - - ,** Fruits - - ,*** Blueberries - - ,*** Grapes - - ,** Vegetables - - ,*** Carrots - - ,*** Potatoes -#+END_SRC - -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 =Fruits= heading 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 [[id:6935361a-9e1d-48ec-8d17-876a90b90f50][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] -:PROPERTIES: -:ID: 4db73c1c-a4ed-425e-9e38-8d334ed03e1e -:END: - -This would be useful for having a menu of saved queries as Org links, or even bookmarking saved queries. - -**** TODO For saved queries - -**** DONE 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: - -#+BEGIN_SRC org - [[org-ql-search:property:author="AUTHOR"]] -#+END_SRC - -[2020-11-08 Sun 23:22] Seems to work! - -#+BEGIN_SRC elisp :results silent - ;;;; 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)) -#+END_SRC - -Tested on these queries: - -#+BEGIN_SRC org - + [[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]] -#+END_SRC - -[2020-11-10 Tue 00:35] I'd like to support other parameters to the search, like grouping and sorting, so: - -#+BEGIN_SRC elisp :results silent - ;;;; 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)) -#+END_SRC - -That seems to work, like: - -#+BEGIN_SRC org - [[org-ql-search:property:author="Chris%20Wellons"?super-groups=((:auto-outline-path%20t))]] -#+END_SRC - -[2020-11-10 Tue 01:34] Okay, this seems to take care of all parameters: - -#+BEGIN_SRC elisp - (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)) -#+END_SRC - -**** DONE For all parameters - -*** DONE Bookmarks - -[2020-11-08 Sun 23:25] Already done in =e5b4cd106558790563af26a8e32ec9508f904855=. - -*** DONE Access saved query from saved query list - -*** DONE Save query from ql-agenda buffer - -** PROJECT [#B] Group tag support -:PROPERTIES: -:milestone: 0.6 -:END: - -*** UNDERWAY Benchmarking tags searches without and with new group-tags support - -#+BEGIN_SRC elisp - (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)) - )) -#+END_SRC - -#+RESULTS: -| Form | x faster than next | Total runtime | # of GCs | Total GC runtime | -|----------------------------+--------------------+---------------+----------+------------------| -| without group-tags support | slowest | 5.512271 | 0 | 0 | - -#+BEGIN_SRC elisp - (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)) - )) -#+END_SRC - -#+RESULTS: -| 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=. - -#+BEGIN_SRC elisp - (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))) - )) -#+END_SRC - -#+RESULTS: -| 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 =result= variable - -** 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). - -#+BEGIN_SRC elisp - (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)))) -#+END_SRC - -** PROJECT [#B] Timeline view -:PROPERTIES: -:ID: 00573552-ffe9-4608-8904-7f6c73246b6d -:END: - -e.g. as mentioned by Samuel Wales at https://lists.gnu.org/archive/html/emacs-orgmode/2019-08/msg00330.html. Prototype code: - -#+BEGIN_SRC elisp - (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))) -#+END_SRC - -Another, more up-to-date implementation: - -#+BEGIN_SRC elisp - ;; 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") -#+END_SRC - -[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= -:PROPERTIES: -:milestone: future -:END: - -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: - -#+BEGIN_SRC elisp - (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)) -#+END_SRC - -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 - -**** [[https://github.com/m2ym/direx-el][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 -:PROPERTIES: -:milestone: 0.6 -:END: - -[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?) - -*** DONE [#A] Implement dynamic blocks - -+ *Tasks* - - [X] Merge code - - [X] Document the feature - -For example, [[https://egli.dev/posts/using-org-mode-for-meeting-minutes/][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%20blocks][info:org#Dynamic blocks]] - -#+BEGIN_SRC elisp - (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)))) -#+END_SRC - -[2020-11-09 Mon 22:35] This works pretty well! For example: - -#+BEGIN_SRC org - ,#+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> - -#+END_SRC - -[2020-11-10 Tue 00:03] I think it's ready to merge now. - -*** DONE [#A] Org block to insert results of queries with links to entries -:PROPERTIES: -:ID: 422754bb-3a7c-4dbf-b303-4056d3cafb7e -:END: - -[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 [#C] [[https://github.com/magit/transient/issues/76][New Transient transient-lisp-variable class]] :compatibility: - -[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: - -1. Equivalent queries will return the same results from the cache. -2. 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 =TODO= items, the ~(todo "TODO")~ selector would convert to a preamble that would quickly search through the file. But if there were a thousand =TODO= items, it wouldn't be as much of a benefit, and a ~(regexp "something")~ selector's preamble might be much faster, depending on how many times =something= appears 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. - -** UNDERWAY [#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. - -#+BEGIN_SRC elisp - (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)))) -#+END_SRC - -** DONE [#A] Views: Multiple sorters are not preserved :bug: -:PROPERTIES: -:milestone: 0.5 -:url: https://github.com/alphapapa/org-ql/issues/136 -:END: - -** DONE [#A] Make dynamic blocks warn about sexp queries -:PROPERTIES: -:milestone: 0.5 -:END: -:LOGBOOK: -CLOCK: [2020-11-16 Mon 22:56]--[2020-11-17 Tue 00:25] => 1:29 -:END: - -[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. - -** DONE [#A] Add Emacs 27.1 to =test.yml= -:PROPERTIES: -:milestone: 0.5 -:END: - -[2020-11-16 Mon 05:22] Also releasing =makem.sh= 0.2 with this change. - -** DONE [#A] Fix =org-ql-view--link-open= on Org 9.3+ :compatibility:bug: -:PROPERTIES: -:milestone: 0.5 -:url: https://github.com/alphapapa/org-ql/issues/147 -:END: - -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=: - -#+BEGIN_SRC elisp - (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))) -#+END_SRC - -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. - -*** DONE [#A] Write tests for storing/opening links -:PROPERTIES: -:milestone: 0.5 -:END: - -[2020-11-16 Mon 05:11] Took way longer than I expected. I hope it was worth it. - -*** DONE [#A] Check Org release notes for link changes - -[2020-11-13 Fri 22:44] From [[https://www.orgmode.org/Changes_old.html][the changelog]]: - -#+BEGIN_QUOTE -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. -#+END_QUOTE - -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? - - -** DONE [#A] Fix query-sexp-to-string function's handling of, e.g. =descendants= :bug: -:PROPERTIES: -:milestone: 0.5 -:END: - -[2020-11-14 Sat 20:45] Fixed in =89ff02a1501b53b4e20cdc66a8fcaa37e7d15734=. - -** DONE [#A] Helm command - -In branch =wip/helm-org-ql=. Works really well, should add it and demonstrate it. - -*** DONE Add - -*** DONE Demonstrate - -*** DONE 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. - -#+BEGIN_SRC elisp - (-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) -#+END_SRC - -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: - -#+BEGIN_SRC elisp - (-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) -#+END_SRC - -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: - -#+BEGIN_SRC elisp - (-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")) -#+END_SRC - -Well, a bit of fiddling (lots of trial-and-error required) produced this: - -#+BEGIN_SRC elisp - (-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"))) -#+END_SRC - -That seems pretty usable! - -** DONE [#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. - -** DONE Byte-compile lambdas -CLOSED: [2018-05-09 Wed 17:30] -:LOGBOOK: -- State "DONE" from [2018-05-09 Wed 17:30] -:END: - -=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: - -#+BEGIN_SRC elisp - (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))))) -#+END_SRC - -#+RESULTS: -| 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: - -#+BEGIN_SRC elisp - (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))))) -#+END_SRC - -#+RESULTS: -| 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... - -#+BEGIN_SRC elisp - (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))))) -#+END_SRC - -#+RESULTS: -| 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. - -** DONE 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: - -#+BEGIN_QUOTE -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. -#+END_QUOTE - -[2019-09-26 Thu 21:31] Handled with the tag caching recently implemented. - -** DONE [#B] Dual matching with regexp and predicates -:PROPERTIES: -:ID: 39972bb5-fdd0-4754-93ba-c85796a67ccf -:END: - -/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: - -#+BEGIN_SRC elisp - (org-ql (org-agenda-files) - (and (regexp "lisp") - (scheduled < today))) -#+END_SRC - -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 [[https://github.com/alphapapa/emacs-package-dev-handbook#bench-multi-lets][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 - -#+BEGIN_SRC elisp - (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))))))) -#+END_SRC - -#+BEGIN_SRC elisp - (org-ql-preamble-bench :query (regexp "Emacs") :times 100) -#+END_SRC - -#+RESULTS: -| 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 | - -#+BEGIN_SRC elisp - (org-ql-preamble-bench :file "~/org/inbox.org" :query (regexp "Emacs") :times 5) -#+END_SRC - -#+RESULTS: -| 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 | - -#+BEGIN_SRC elisp - (org-ql-preamble-bench :file "~/org/inbox.org" :query (and (regexp "Emacs") (todo)) :times 5) -#+END_SRC - -#+RESULTS: -| 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 | - -#+BEGIN_SRC elisp - (org-ql-preamble-bench :file "~/org/inbox.org" :query (and (regexp "Emacs") (todo) (scheduled)) :times 5) -#+END_SRC - -#+RESULTS: -| 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 | - -#+BEGIN_SRC elisp - (org-ql-preamble-bench :file "~/org/inbox.org" :query (todo "WAITING") :times 2) -#+END_SRC - -#+RESULTS: -| 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! - -** DONE Operate on list of heading positions -CLOSED: [2018-05-10 Thu 15:02] -:LOGBOOK: -- State "DONE" from [2018-05-10 Thu 15:02] -:END: - -[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=. - -** DONE Use macros for =date= -CLOSED: [2018-05-10 Thu 14:59] -:LOGBOOK: -- State "DONE" from [2018-05-10 Thu 14:59] -:END: - -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 - -** Commit - -*** Fix - -+ [ ] Check repo for closing issue -+ [ ] Check this file for closing task -+ [ ] Lint -+ [ ] Test -+ [ ] Commit and push - -* Examples / testing -:PROPERTIES: -:TOC: :include descendants :depth 1 -:END: -:CONTENTS: -- [[#property-matching][Property matching]] -- [[#regexp-matching][Regexp matching]] -- [[#screenshot-code][Screenshot code]] -- [[#sorting][Sorting]] -:END: - -#+BEGIN_SRC elisp - (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))) -#+END_SRC - -** Property matching - -#+BEGIN_SRC elisp - (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")) -#+END_SRC - -** Regexp matching - -#+BEGIN_SRC elisp - (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")) -#+END_SRC - -** Screenshot code - -#+BEGIN_SRC elisp - (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))))) -#+END_SRC - -** Sorting - -#+BEGIN_SRC elisp - (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)) -#+END_SRC - -* In the wild -:PROPERTIES: -:TOC: :include descendants :depth 1 -:END: -:CONTENTS: -- [[#alois-janicek][Alois Janicek]] -- [[#benson-chu][Benson Chu]] -:END: - -** [[https://github.com/AloisJanicek/.doom.d-2nd][Alois Janicek]] - -*** [[https://github.com/AloisJanicek/.doom.d-2nd/commit/41ed1080f6f90463fc1f1d7e47cef9864756867c][further tweaking org-ql views · AloisJanicek/.doom.d-2nd@41ed108 · GitHub]] - -#+BEGIN_SRC elisp :eval never -;; 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") - ) -#+END_SRC - -** [[https://github.com/pestctrl/emacs-config][Benson Chu]] - -*** [[https://github.com/pestctrl/emacs-config/commit/fa3068003373a0c93e23c728b5dbad2d7c11e2e1][Experimental agenda view with org-ql · pestctrl/emacs-config@fa30680 · GitHub]] - -#+BEGIN_SRC elisp :eval never - ("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))))))))))) -#+END_SRC - -* Profiling -:PROPERTIES: -:TOC: :include descendants :depth 1 -:END: - -:CONTENTS: -- [[#2019-08-29-thu-0624--benchmarking-org-ql-compared-to-re-search-forward-for-getting-headings-in-buffer][{2019-08-29 Thu 06:24} Benchmarking org-ql compared to re-search-forward for getting headings in buffer]] -- [[#caching-of-inherited-tags][Caching of inherited tags]] -- [[#intersecting-query-results][Intersecting query results]] -- [[#more-profiling][More profiling]] -- [[#preambles][Preambles]] -- [[#profiling-flet-across-all-agenda-files][Profiling flet across all agenda files]] -- [[#profiling-flet-on-a-single-file][Profiling flet on a single file]] -- [[#profiling-org-trust-scanner-tags][Profiling org-trust-scanner-tags]] -- [[#profiling-position-based][Profiling position-based]] -- [[#profiling-tags-matching][Profiling tags matching]] -- [[#using-org-element-parse-buffer][Using org-element-parse-buffer]] -- [[#withwithout-tsel][with/without ts.el]] -:END: - - -** [2019-08-29 Thu 06:24] Benchmarking org-ql compared to re-search-forward for getting headings in buffer -:PROPERTIES: -:ID: fcc09229-ed24-42eb-a1fd-31d8f7d4c8d5 -:END: - -Minimal difference, and that's a very large file, too. On smaller files it's thousandths of a second. - -#+BEGIN_SRC elisp - (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)))))))) -#+END_SRC - -#+RESULTS: -| 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. - -#+BEGIN_SRC elisp - (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)))))))) -#+END_SRC - -Benchmark results: - -#+BEGIN_SRC elisp - (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))))))) -#+END_SRC - -#+RESULTS: -| 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. - -#+BEGIN_SRC elisp - (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))))) -#+END_SRC - -#+RESULTS: -| 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. - -#+BEGIN_SRC elisp - (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))))))) -#+END_SRC - -#+RESULTS: -| 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 | - -#+BEGIN_SRC elisp - -;; 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))))))) -#+END_SRC - -#+RESULTS: -| 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: - -#+BEGIN_SRC elisp - (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) - - ))))) -#+END_SRC - -#+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: - -#+BEGIN_SRC elisp - (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))))))) -#+END_SRC - -#+RESULTS: -| 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. - -#+BEGIN_SRC elisp - (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)))))) -#+END_SRC - -#+RESULTS: -| 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. - -#+BEGIN_SRC elisp :results silent - (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)))))) -#+END_SRC - -*** =closed= - -#+BEGIN_SRC elisp - (org-ql-preamble-bench :times 1 - :file "~/org/inbox.org" - :query (closed)) -#+END_SRC - -#+RESULTS: -| 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 | - -#+BEGIN_SRC elisp - (org-ql-preamble-bench :times 1 - :file "~/org/inbox.org" - :query (closed <= "2019-01-01")) -#+END_SRC - -#+RESULTS: -| 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= - -#+BEGIN_SRC elisp - (org-ql-preamble-bench :times 1 - :file "~/org/inbox.org" - :query (deadline)) -#+END_SRC - -#+RESULTS: -| 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 | - -#+BEGIN_SRC elisp - (org-ql-preamble-bench :times 1 - :file "~/org/inbox.org" - :query (deadline <= "2019-01-01")) -#+END_SRC - -#+RESULTS: -| 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= - -#+BEGIN_SRC elisp - (org-ql-preamble-bench :times 1 - :file "~/org/inbox.org" - :query (habit)) -#+END_SRC - -#+RESULTS: -| 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= - -#+BEGIN_SRC elisp - (org-ql-preamble-bench :times 1 - :file "~/org/inbox.org" - :query (level 1)) -#+END_SRC - -#+RESULTS: -| 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= - -#+BEGIN_SRC elisp - (org-ql-preamble-bench :times 1 - :file "~/org/inbox.org" - :query (property "agenda-group")) -#+END_SRC - -#+RESULTS: -| 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 | - -#+BEGIN_SRC elisp - (org-ql-preamble-bench :times 1 - :file "~/org/inbox.org" - :query (property "ID")) -#+END_SRC - -#+RESULTS: -| 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 | - -#+BEGIN_SRC elisp - (org-ql-preamble-bench :times 1 - :file "~/org/inbox.org" - :query (property "agenda-group" "plans")) -#+END_SRC - -#+RESULTS: -| 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= - -#+BEGIN_SRC elisp - (org-ql-preamble-bench :times 1 - :file "~/org/inbox.org" - :query (scheduled)) -#+END_SRC - -#+RESULTS: -| 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 | - -#+BEGIN_SRC elisp - (org-ql-preamble-bench :times 1 - :file "~/org/inbox.org" - :query (scheduled <= "2019-01-01")) -#+END_SRC - -#+RESULTS: -| 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. - -#+BEGIN_SRC elisp - (let ((org-use-tag-inheritance t)) - (org-ql-preamble-bench :times 1 - :file "~/org/inbox.org" - :query (tags "Emacs"))) -#+END_SRC - -#+RESULTS: -| 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 | - -#+BEGIN_SRC elisp - (let ((org-use-tag-inheritance nil)) - (org-ql-preamble-bench :times 1 - :file "~/org/inbox.org" - :query (tags "Emacs"))) -#+END_SRC - -#+RESULTS: -| 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~ - -#+BEGIN_SRC elisp - (org-ql-preamble-bench :times 1 - :file "~/org/inbox.org" - :query (ts)) -#+END_SRC - -#+RESULTS: -| 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 | - -#+BEGIN_SRC elisp - (org-ql-preamble-bench :times 1 - :file "~/org/inbox.org" - :query (ts :from "2019-01-01")) -#+END_SRC - -#+RESULTS: -| 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 | - -#+BEGIN_SRC elisp - (org-ql-preamble-bench :times 1 - :file "~/org/inbox.org" - :query (ts :from "2017-01-01")) -#+END_SRC - -#+RESULTS: -| 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. - -#+BEGIN_SRC elisp - (org-ql-preamble-bench :times 10 - :query (ts :from "2017-01-01")) -#+END_SRC - -#+RESULTS: -| 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 | - -#+BEGIN_SRC elisp - (org-ql-preamble-bench :times 1 - :file "~/org/inbox.org" - :query (ts :to "2010-01-01")) -#+END_SRC - -#+RESULTS: -| 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~ - -#+BEGIN_SRC elisp - (org-ql-preamble-bench :times 1 - :file "~/org/inbox.org" - :query (ts-a)) -#+END_SRC - -#+RESULTS: -| 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 | - -#+BEGIN_SRC elisp - (org-ql-preamble-bench :times 1 - :file "~/org/inbox.org" - :query (ts-a :from "2017-07-06")) -#+END_SRC - -#+RESULTS: -| 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 | - -#+BEGIN_SRC elisp - (org-ql-preamble-bench :times 1 - :file "~/org/inbox.org" - :query (ts-a :to "2017-07-06")) -#+END_SRC - -#+RESULTS: -| 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 | - -#+BEGIN_SRC elisp - (org-ql-preamble-bench :times 1 - :file "~/org/inbox.org" - :query (ts-a :on "2017-07-06")) -#+END_SRC - -#+RESULTS: -| 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~ - -#+BEGIN_SRC elisp - (org-ql-preamble-bench :times 1 - :file "~/org/inbox.org" - :query (ts-i)) -#+END_SRC - -#+RESULTS: -| 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 | - -#+BEGIN_SRC elisp - (org-ql-preamble-bench :times 1 - :file "~/org/inbox.org" - :query (ts-i :from "2019-07-06")) -#+END_SRC - -#+RESULTS: -| 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 | - -#+BEGIN_SRC elisp - (org-ql-preamble-bench :times 1 - :file "~/org/inbox.org" - :query (ts-i :to "2019-07-06")) -#+END_SRC - -#+RESULTS: -| 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 - -#+BEGIN_SRC elisp - (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)))))) -#+END_SRC - -#+RESULTS: -#+begin_example -mapcar 711 25.608392569 0.0360174297 -org-agenda-ng--agenda 5 24.019318793 4.8038637586 -org-agenda-ng--filter-buffer 40 14.160293256 0.3540073313 -org-agenda-ng--date-p 21595 4.2111783960 0.0001950071 -org-agenda-finalize-entries 5 4.0930243110 0.8186048622 -org-super-agenda--filter-finalize-entries 5 3.937522006 0.7875044012 -org-agenda-ng--todo-p 37080 3.5687476730 9.624...e-05 -org-get-todo-state 37080 3.4737076600 9.368...e-05 -outline-next-heading 34625 3.4689080650 0.0001001850 -re-search-forward 42280 3.0743315830 7.271...e-05 -org-agenda-ng--format-element 1180 2.9511605820 0.0025009835 -org-element-headline-parser 1180 2.6757063699 0.0022675477 -org-super-agenda--group-items 5 2.187362092 0.4374724183 -org-super-agenda--group-dispatch 70 2.184685662 0.0312097951 -org-entry-get 22730 2.0711872869 9.112...e-05 -org-entry-properties 21595 1.8958912070 8.779...e-05 -org-super-agenda--group-tag 25 1.8498977799 0.0739959111 -org-element-timestamp-parser 3785 1.8234333229 0.0004817525 -org-parse-time-string 7560 1.7121709579 0.0002264776 -org-element--get-time-properties 1180 1.1814058020 0.0010011913 -#+end_example - -*** Without flet - -#+BEGIN_SRC elisp - (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)))))) -#+END_SRC - -#+RESULTS: -#+begin_example -mapcar 711 26.910164986 0.0378483333 -org-agenda-ng--agenda 5 21.012501837 4.2025003674 -org-agenda-ng--filter-buffer 40 13.751964650 0.3437991162 -org-agenda-ng--todo-p 37080 5.8788306440 0.0001585445 -org-agenda-ng--format-element 1180 4.5712275970 0.0038739216 -org-get-todo-state 37080 4.1661659069 0.0001123561 -org-agenda-ng--date-p 21595 4.1442710769 0.0001919088 -org-entry-get 22730 2.8275069239 0.0001243953 -org-entry-properties 21595 2.6558403739 0.0001229840 -outline-next-heading 34625 2.0894695999 6.034...e-05 -org-element-headline-parser 1180 1.9110445780 0.0016195293 -re-search-forward 42280 1.6994989150 4.019...e-05 -org-agenda-ng--add-faces 1180 1.6172592580 0.0013705586 -org-agenda-ng--add-scheduled-face 1180 1.607386145 0.0013621916 -org-get-tags-at 1180 1.1521010509 0.0009763568 -org-back-to-heading 64530 1.1005834200 1.705...e-05 -org-up-heading-safe 2360 1.0182265390 0.0004314519 -outline-back-to-heading 64530 1.0086056729 1.563...e-05 -org-parse-time-string 7560 0.8314918499 0.0001099856 -org-time-string-to-absolute 3780 0.8277485280 0.0002189810 -#+end_example - - -** 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 - -#+BEGIN_SRC elisp - (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)))))) -#+END_SRC - -#+RESULTS: -#+begin_example -mapcar 526 3.7898506779 0.0072050393 -org-agenda-ng--agenda 5 2.7695176850 0.5539035370 -org-agenda-ng--filter-buffer 5 1.414347774 0.2828695548 -org-agenda-ng--format-element 265 0.8871611419 0.0033477778 -org-get-tags-at 265 0.7891641319 0.0029779778 -org-up-heading-safe 1150 0.7581951110 0.0006593000 -re-search-backward 3700 0.5948686769 0.0001607753 -org-agenda-ng--todo-p 6690 0.5840980579 8.730...e-05 -org-get-todo-state 6690 0.5666448919 8.470...e-05 -org-agenda-ng--date-p 5940 0.5196037069 8.747...e-05 -org-entry-get 6195 0.4144106150 6.689...e-05 -org-entry-properties 5940 0.3640680380 6.129...e-05 -org-element-headline-parser 265 0.2810144920 0.0010604320 -outline-next-heading 6195 0.2495287770 4.027...e-05 -org-back-to-heading 14565 0.1959557380 1.345...e-05 -re-search-forward 7850 0.1933439489 2.462...e-05 -outline-back-to-heading 14565 0.1753121230 1.203...e-05 -org-outline-level 2300 0.1676228200 7.287...e-05 -org-agenda-finalize-entries 5 0.1607656930 0.0321531386 -org-super-agenda--filter-finalize-entries 5 0.1316961509 0.0263392301 -#+end_example - -*** Without flet - -#+BEGIN_SRC elisp - (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)))))) -#+END_SRC - -#+RESULTS: -#+begin_example -mapcar 526 3.7766218089 0.0071798893 -org-agenda-ng--agenda 5 2.75718831 0.551437662 -org-agenda-ng--filter-buffer 5 1.402551392 0.2805102784 -org-agenda-ng--format-element 265 0.8864161399 0.0033449665 -org-get-tags-at 265 0.7896260759 0.0029797210 -org-up-heading-safe 1150 0.7589292910 0.0006599385 -re-search-backward 3700 0.5956338739 0.0001609821 -org-agenda-ng--todo-p 6690 0.5781650060 8.642...e-05 -org-get-todo-state 6690 0.5603983020 8.376...e-05 -org-agenda-ng--date-p 5940 0.5209897369 8.770...e-05 -org-entry-get 6195 0.4158440950 6.712...e-05 -org-entry-properties 5940 0.3640524090 6.128...e-05 -org-element-headline-parser 265 0.2810144710 0.0010604319 -outline-next-heading 6195 0.2485497380 4.012...e-05 -org-back-to-heading 14565 0.1957209180 1.343...e-05 -re-search-forward 7850 0.1927130979 2.454...e-05 -outline-back-to-heading 14565 0.1751091780 1.202...e-05 -org-outline-level 2300 0.1680958539 7.308...e-05 -org-agenda-finalize-entries 5 0.1610422239 0.0322084448 -org-super-agenda--filter-finalize-entries 5 0.132423043 0.0264846085 -#+end_example - - -** Profiling =org-trust-scanner-tags= - -[2018-05-10 Thu 12:59] Turned on =org-trust-scanner-tags=, going to try profiling again: - -#+BEGIN_SRC elisp - ;; (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"))) -#+END_SRC - -#+RESULTS: -| 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: - -#+BEGIN_SRC elisp - ;; (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"))) -#+END_SRC - -#+RESULTS: -| 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 - -#+BEGIN_SRC elisp - (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)) -#+END_SRC - - -*** ng-flet - -#+BEGIN_SRC elisp - (elp-profile 5 (org-agenda-ng--test-agenda-today)) -#+END_SRC - -#+RESULTS: -#+begin_example -mapcar 121 0.1292609089 0.0010682719 -org-agenda-ng--test-agenda-today 5 0.0860146149 0.017202923 -org-agenda-ng--agenda 5 0.0858901769 0.0171780353 -org-agenda-ng--format-element 75 0.0308815090 0.0004117534 -org-agenda-ng--filter-buffer 5 0.026709027 0.0053418054 -org-agenda-ng--date-p 455 0.0210552310 4.627...e-05 -org-element-headline-parser 75 0.016209908 0.0002161321 -org-get-tags-at 75 0.008953666 0.0001193822 -org-agenda-ng--add-faces 75 0.0072834109 9.711...e-05 -org-element-timestamp-interpreter 150 0.0068781430 4.585...e-05 -org-entry-get 290 0.0060815609 2.097...e-05 -org-up-heading-safe 210 0.005708647 2.718...e-05 -org-agenda-finalize-entries 5 0.005201221 0.0010402442 -org-element-timestamp-parser 150 0.005191617 3.461078e-05 -org-entry-properties 290 0.0048787450 1.682...e-05 -org-element--get-time-properties 75 0.004112675 5.483...e-05 -org-agenda-ng--add-deadline-face 75 0.0039314910 5.241...e-05 -org-back-to-heading 725 0.0037559990 5.180...e-06 -org-agenda-ng--add-scheduled-face 75 0.0031766149 4.235...e-05 -org-parse-time-string 300 0.0031740200 1.058...e-05 -#+end_example - -*** ng-funcall - -#+BEGIN_SRC elisp - (elp-profile 5 (org-agenda-ng--test-agenda-today)) -#+END_SRC - -#+RESULTS: -#+begin_example -mapcar 121 0.1296645480 0.0010716078 -org-agenda-ng--test-agenda-today 5 0.086714029 0.0173428058 -org-agenda-ng--agenda 5 0.086584611 0.0173169222 -org-agenda-ng--format-element 75 0.0307461019 0.0004099480 -org-agenda-ng--filter-buffer 5 0.027136826 0.0054273652 -org-agenda-ng--date-p 455 0.0213037090 4.682...e-05 -org-element-headline-parser 75 0.016251755 0.0002166900 -org-get-tags-at 75 0.008959605 0.0001194614 -org-agenda-ng--add-faces 75 0.0072381410 9.650...e-05 -org-element-timestamp-interpreter 150 0.0069832960 4.655...e-05 -org-entry-get 290 0.0061220340 2.111...e-05 -org-up-heading-safe 210 0.0057036860 2.716...e-05 -org-agenda-finalize-entries 5 0.005372899 0.0010745798 -org-element-timestamp-parser 150 0.0050518689 3.367...e-05 -org-entry-properties 290 0.0049517209 1.707...e-05 -org-agenda-ng--add-deadline-face 75 0.0039273909 5.236...e-05 -org-element--get-time-properties 75 0.0039059429 5.207...e-05 -org-back-to-heading 725 0.0037793259 5.212...e-06 -org-parse-time-string 300 0.0032196259 1.073...e-05 -org-agenda-ng--add-scheduled-face 75 0.0031410580 4.188...e-05 -#+end_example - -*** orig - -Make sure to kill any existing agenda buffers first. - -#+BEGIN_SRC elisp - (elp-profile 1 (org-agenda-list nil nil 'week)) -#+END_SRC - -#+RESULTS: -#+begin_example -org-agenda-list 1 9.693596196 9.693596196 -org-agenda-get-day-entries 56 8.630330659 0.1541130474 -org-agenda-get-scheduled 56 6.6207980570 0.1182285367 -org-is-habit-p 2792 2.2907458449 0.0008204677 -org-entry-get 2798 2.287390186 0.0008175090 -org-agenda--timestamp-to-absolute 7708 2.0970420100 0.0002720604 -org-agenda-get-deadlines 56 1.6941886389 0.0302533685 -org-at-planning-p 4399 1.3993312159 0.0003181021 -org--property-local-values 2793 1.2699226760 0.0004546805 -org-get-property-block 2794 1.2182695930 0.0004360306 -org-time-string-to-absolute 7708 1.1513844880 0.0001493752 -org-inlinetask-in-task-p 6969 1.139932302 0.0001635718 -org-parse-time-string 7880 1.0635759220 0.0001349715 -org-closest-date 3864 1.0383435800 0.0002687224 -re-search-forward 15199 0.9607921779 6.321...e-05 -org-back-to-heading 12667 0.8564486210 6.761...e-05 -outline-back-to-heading 12667 0.8362207570 6.601...e-05 -line-beginning-position 10333 0.7998346869 7.740...e-05 -org-agenda-format-item 279 0.7552402350 0.0027069542 -re-search-backward 16726 0.5694224969 3.404...e-05 -#+end_example - -** Profiling tags matching - -*** ng - -#+BEGIN_SRC elisp - (elp-profile 1 nil - (org-agenda-ng "~/org/main.org" - (tags "computer"))) -#+END_SRC - -#+RESULTS: -| 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 - -#+BEGIN_SRC elisp - (elp-profile 1 nil - (org-agenda-ng "~/org/main.org" - (tags "computer"))) -#+END_SRC - -#+RESULTS: -| 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 - -#+BEGIN_SRC elisp - (elp-profile 1 nil - (with-current-buffer "main.org" -(org-tags-view nil "computer"))) -#+END_SRC - -#+RESULTS: -| 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 - -#+BEGIN_SRC elisp - (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))))))) -#+END_SRC - -[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 - -#+BEGIN_SRC elisp - (elp-profile 1 (org-agenda-ng--test-agenda-today)) -#+END_SRC - -#+RESULTS: -#+begin_example -org-element--parse-elements 5832 18.501891926 0.0031724780 -mapcar 98 6.3412930759 0.0647070722 -org-agenda-ng--test-agenda-today 1 6.30112088 6.30112088 -org-agenda-ng--agenda-multi 1 6.301086333 6.301086333 -org-agenda-ng--get-entries 8 6.249823971 0.7812279963 -mapc 2803 5.9078545849 0.0021076898 -org-element-parse-buffer 8 4.796204625 0.5995255781 -org-element--current-element 6557 3.7164850469 0.0005667965 -org-element-headline-parser 6557 3.5548915590 0.0005421521 -org-end-of-subtree 6557 1.3663438270 0.0002083794 -org-agenda-ng--filter-tree 8 1.3661297829 0.1707662228 -org-element-map 8 1.365995685 0.1707494606 -line-end-position 9503 0.4900104040 5.156...e-05 -org-at-heading-p 12389 0.4574876539 3.692...e-05 -re-search-forward 20193 0.4566419319 2.261...e-05 -outline-on-heading-p 19286 0.4559736580 2.364...e-05 -org-outline-level 6775 0.3970647569 5.860...e-05 -org-back-to-heading 6897 0.3689033409 5.348...e-05 -outline-back-to-heading 6897 0.3587580889 5.201...e-05 -line-beginning-position 11702 0.2887473860 2.467...e-05 -#+end_example - -*** orig - -Make sure to kill any existing agenda buffers first. - -#+BEGIN_SRC elisp - (elp-profile 1 (org-agenda-list nil nil 'week)) -#+END_SRC - -#+RESULTS: -#+begin_example -org-agenda-list 1 1.717467214 1.717467214 -org-agenda-get-day-entries 7 1.124906724 0.1607009605 -org-agenda-get-scheduled 7 0.9354116170 0.1336302310 -org-get-tags-at 62 0.5598817790 0.0090303512 -org-up-heading-safe 262 0.5531687240 0.0021113310 -org-back-to-heading 2646 0.5316139889 0.0002009123 -org-agenda-finalize 1 0.485185818 0.485185818 -org-agenda-skip 749 0.4059732339 0.0005420203 -org-at-planning-p 921 0.2137141959 0.0002320458 -org-is-habit-p 574 0.1953095319 0.0003402605 -org-entry-get 579 0.195275877 0.0003372640 -org--property-local-values 574 0.1899641870 0.0003309480 -org-get-property-block 574 0.1789142650 0.0003116973 -re-search-backward 3586 0.1769497870 4.934...e-05 -org-inlinetask-in-task-p 1495 0.1639709820 0.0001096795 -outline-back-to-heading 2646 0.1387423440 5.243...e-05 -re-search-forward 3177 0.1353034909 4.258...e-05 -org-agenda-get-deadlines 7 0.1341101260 0.0191585894 -line-beginning-position 2041 0.1000089939 4.899...e-05 -org-get-todo-state 749 0.070545971 9.418...e-05 -org-agenda-prepare 1 0.052788647 0.052788647 -org-agenda-prepare-buffers 1 0.050402675 0.050402675 -org-agenda-get-timestamps 7 0.0428353419 0.0061193345 -org-agenda--timestamp-to-absolute 1498 0.0414448099 2.766...e-05 -org-time-string-to-absolute 1498 0.0385546620 2.573...e-05 -org-agenda-finalize-entries 7 0.03400573 0.0048579614 -org-super-agenda--finalize-entries 7 0.0339819269 0.0048545609 -org-outline-level 505 0.0335176070 6.637...e-05 -org-super-agenda--group-items 7 0.0268096709 0.0038299529 -org-super-agenda--group-dispatch 84 0.024216379 0.0002882902 -org-parse-time-string 1572 0.0211881319 1.347...e-05 -org-closest-date 749 0.0178231690 2.379...e-05 -org-before-first-heading-p 578 0.0142076310 2.458...e-05 -org-refresh-category-properties 1 0.013815263 0.013815263 -org-in-src-block-p 753 0.0135235599 1.795...e-05 -org-refresh-stats-properties 1 0.012230309 0.012230309 -org-habit-parse-todo 5 0.0121474539 0.0024294907 -org-get-limited-outline-regexp 1502 0.0100960829 6.721...e-06 -mapcar 309 0.009495786 3.073...e-05 -org-super-agenda--group-habit 7 0.009318616 0.0013312308 -string-match 6253 0.0090207890 1.442...e-06 -org-super-agenda--group-dispatch-and 7 0.0067444 0.0009634857 -org-agenda-get-blocks 7 0.0060974240 0.0008710605 -outline-on-heading-p 2655 0.0060741090 2.287...e-06 -org-agenda-get-sexps 7 0.0057924309 0.0008274901 -org-super-agenda--group-regexp 7 0.005592509 0.0007989298 -org-refresh-properties 2 0.005461118 0.002730559 -org-super-agenda--get-item-entry 31 0.004812809 0.0001552519 -org-agenda-align-tags 1 0.004677699 0.004677699 -org-set-regexps-and-options 1 0.004533983 0.004533983 -org--setup-collect-keywords 2 0.004499168 0.002249584 -org-agenda-format-item 31 0.0039898020 0.0001287032 -org-date-to-gregorian 420 0.0039648049 9.440...e-06 -org-agenda-highlight-todo 31 0.0039479179 0.0001273521 -string-to-number 8202 0.0031924329 3.892...e-07 -org-end-of-subtree 12 0.003047285 0.0002539404 -org-super-agenda--group-tag 35 0.002941374 8.403...e-05 -org-inlinetask-outline-regexp 1495 0.002887697 1.931...e-06 -org-get-wdays 749 0.0027946290 3.731...e-06 -org-refresh-effort-properties 1 0.002750279 0.002750279 -org-entry-beginning-position 31 0.002345909 7.567...e-05 -sort 25 0.0021000059 8.400...e-05 -mapc 445 0.0018132390 4.074...e-06 -line-end-position 72 0.00177188 2.460...e-05 -mapconcat 341 0.0016574119 4.860...e-06 -org-entries-lessp 60 0.0016203450 2.700...e-05 -concat 336 0.0016188640 4.818...e-06 -org-habit-insert-consistency-graphs 1 0.001368346 0.001368346 -org-add-props 137 0.001291149 9.424...e-06 -org-element-at-point 1 0.001266403 0.001266403 -org-agenda-new-marker 62 0.001253987 2.022...e-05 -outline-next-heading 42 0.001123596 2.675...e-05 -org-split-string 148 0.0011049120 7.465...e-06 -org-element--parse-to 1 0.001074227 0.001074227 -org-get-scheduled-time 5 0.0010200300 0.000204006 -org-super-agenda--group-todo 21 0.001003225 4.777...e-05 -org-time-string-to-time 69 0.0009947770 1.441...e-05 -org-element--current-element 4 0.0009597410 0.0002399352 -org-entry-properties 5 0.0008807149 0.0001761429 -org-at-date-range-p 172 0.0008412679 4.891...e-06 -org-agenda-mode 1 0.000787997 0.000787997 -org-super-agenda--group-log 14 0.0007874939 5.624...e-05 -buffer-substring 31 0.0007650360 2.467...e-05 -org-heading-components 9 0.00071644 7.960...e-05 -org-entry-end-position 31 0.0007046580 2.273...e-05 -string-prefix-p 789 0.0006732279 8.532...e-07 -org-agenda-skip-eval 1498 0.0006586869 4.397...e-07 -org-get-repeat 5 0.000656577 0.0001313153 -org-activate-bracket-links 3 0.000648713 0.0002162376 -org-agenda-fix-displayed-tags 31 0.000636446 2.053...e-05 -s-join 252 0.0006324150 2.509...e-06 -org-element-keyword-parser 4 0.000530311 0.0001325777 -org-in-commented-heading-p 4 0.00051077 0.0001276925 -buffer-substring-no-properties 308 0.0004763350 1.546...e-06 -org-habit-build-graph 5 0.0004689199 9.3784e-05 -car 1626 0.0004469320 2.748...e-07 -org-activate-plain-links 3 0.000382191 0.000127397 -org-super-agenda--transform-groups 7 0.0003747289 5.353...e-05 -org-get-priority 26 0.0003598600 1.384...e-05 -org-agenda-fontify-priorities 1 0.000357076 0.000357076 -org-super-agenda--group-priority 14 0.00035224 2.516e-05 -org-agenda-prepare-window 1 0.00033704 0.00033704 -org-find-text-property-in-string 316 0.0003191880 1.010...e-06 -org-not-nil 821 0.0003016530 3.674...e-07 -list 940 0.0002995150 3.186...e-07 -org-super-agenda--group-time-grid 7 0.0002954999 4.221...e-05 -org-agenda-format-date-aligned 7 0.0002794120 3.991...e-05 -org-super-agenda--get-tags 103 0.0002654610 2.577...e-06 -delq 513 0.0002634709 5.135...e-07 -org-replace-escapes 5 0.0002382050 4.7641e-05 -car-safe 911 0.0002314199 2.540...e-07 -org-today 48 0.0002232539 4.651...e-06 -org-get-time-of-day 19 0.000219044 1.152...e-05 -org-days-to-iso-week 9 0.0002125040 2.361...e-05 -append 433 0.0002083290 4.811...e-07 -org-get-category 31 0.000198604 6.406...e-06 -string-match-p 31 0.0001787880 5.767...e-06 -org-agenda-today-p 21 0.0001658670 7.898...e-06 -org-super-agenda--get-marker 54 0.0001629399 3.017...e-06 -delete-dups 111 0.0001343219 1.210...e-06 -org-at-heading-p 9 0.000128871 1.4319e-05 -string-equal 361 0.0001168969 3.238...e-07 -org-super-agenda--make-agenda-header 13 0.0001151760 8.859...e-06 -org-add-prop-inherited 142 0.0001142290 8.044...e-07 -org-check-agenda-file 8 0.000110448 1.3806e-05 -org-agenda-get-day-face 7 0.000110437 1.577...e-05 -org-get-agenda-file-buffer 8 0.000107414 1.342675e-05 -org-agenda-files 3 0.000102652 3.421...e-05 -org-downcase-keep-props 90 0.0001008230 1.120...e-06 -org-super-agenda--get-priority-cookie 15 9.460...e-05 6.307e-06 -org-get-todo-face 31 9.423...e-05 3.039...e-06 -org-find-base-buffer-visiting 8 8.7492e-05 1.09365e-05 -org-habit-get-faces 140 8.080...e-05 5.771...e-07 -org-remove-uninherited-tags 176 7.580...e-05 4.307...e-07 -goto-char 31 7.1014e-05 2.290...e-06 -org-super-agenda--transform-group-order 7 6.5543e-05 9.363...e-06 -org-agenda-add-time-grid-maybe 7 5.703...e-05 8.147...e-06 -org-compile-prefix-format 1 5.2164e-05 5.2164e-05 -org-link-unescape 4 4.4823e-05 1.120575e-05 -listp 133 3.960...e-05 2.977...e-07 -org-habit-duration-to-days 5 3.9446e-05 7.8892e-06 -org-make-options-regexp 1 3.6054e-05 3.6054e-05 -org-agenda-mark-header-line 1 3.3066e-05 3.3066e-05 -org-element--collect-affiliated-keywords 4 3.2661e-05 8.16525e-06 -org-habit-get-priority 5 2.9857e-05 5.9714e-06 -org-agenda-get-category-icon 31 2.673...e-05 8.624...e-07 -org-face-from-face-or-color 61 2.1803e-05 3.574...e-07 -org-reduced-level 40 2.1692e-05 5.423e-07 -org-link-get-parameter 26 2.062...e-05 7.930...e-07 -buffer-name 57 1.9875e-05 3.486...e-07 -org-defkey 5 1.9835e-05 3.966...e-06 -string-to-char 56 1.929...e-05 3.446...e-07 -org-string-nw-p 5 1.7894e-05 3.578...e-06 -s-wrap 7 1.6955e-05 2.422...e-06 -org-link-expand-abbrev 2 1.5491e-05 7.7455e-06 -map-keymap 2 1.4987e-05 7.4935e-06 -search-forward 7 1.403...e-05 2.004...e-06 -string-lessp 41 1.394...e-05 3.400...e-07 -org-agenda-reset-markers 1 1.3927e-05 1.3927e-05 -org-agenda-deadline-face 7 1.2592e-05 1.798...e-06 -search-backward 5 1.1253e-05 2.2506e-06 -org-agenda-span-name 5 9.758e-06 1.9516e-06 -outline-previous-heading 1 8.678e-06 8.678e-06 -buffer-modified-p 10 4.618e-06 4.617...e-07 -buffer-file-name 13 4.608...e-06 3.544...e-07 -buffer-live-p 11 4.473e-06 4.066...e-07 -buffer-local-value 10 4.278e-06 4.278...e-07 -buffer-base-buffer 10 3.858e-06 3.858...e-07 -org-agenda-set-mode-name 1 3.118e-06 3.118e-06 -org-remove-flyspell-overlays-in 4 2.926...e-06 7.315...e-07 -buffer-size 8 2.791...e-06 3.489...e-07 -org-key 5 2.707e-06 5.414e-07 -org-unbracket-string 1 2.692e-06 2.692e-06 -save-place-to-alist 1 2.276e-06 2.276e-06 -org-element--cache-put 4 1.907e-06 4.7675e-07 -org-property-inherit-p 2 1.414e-06 7.07e-07 -org-agenda-ndays-to-span 2 1.293...e-06 6.465...e-07 -maphash 1 1.171e-06 1.171e-06 -org-set-sorting-strategy 1 1.165e-06 1.165e-06 -org-file-menu-entry 1 1.12e-06 1.12e-06 -string-width 2 1.093e-06 5.465e-07 -org-time-stamp-format 1 1.054e-06 1.054e-06 -org-agenda-fit-window-to-buffer 1 9.31e-07 9.31e-07 -org-font-lock-add-tag-faces 1 9.13e-07 9.13e-07 -org-agenda-span-to-ndays 1 8.3e-07 8.3e-07 -org-element-property 2 8.12e-07 4.06e-07 -org-agenda-mark-clocking-task 1 8.03e-07 8.03e-07 -org-tag-alist-to-groups 1 7.55e-07 7.55e-07 -org-element-type 1 6.14e-07 6.14e-07 -org-agenda-use-sticky-p 1 4.73e-07 4.73e-07 -mapatoms 1 0 0.0 -#+end_example - -*** Profile org-element-map - -#+BEGIN_SRC elisp - (elp-profile 1 (with-current-buffer (find-buffer-visiting "~/org/main.org") - (org-element-parse-buffer 'headline))) -#+END_SRC - -#+RESULTS: -#+begin_example -org-element--parse-elements 1002 4.1612395469 0.0041529336 -org-element-parse-buffer 1 0.859981956 0.859981956 -org-element--current-element 1225 0.8236391779 0.0006723585 -org-element-headline-parser 1225 0.7952382879 0.0006491741 -org-end-of-subtree 1225 0.5557043549 0.0004536362 -line-end-position 1995 0.0751104350 3.764...e-05 -re-search-forward 3743 0.0516547359 1.380...e-05 -org-outline-level 1225 0.0477962079 3.901...e-05 -org-back-to-heading 1225 0.0469223529 3.830...e-05 -outline-back-to-heading 1225 0.0450936199 3.681...e-05 -org-element--get-node-properties 1225 0.0434517140 3.547...e-05 -line-beginning-position 2003 0.0334129610 1.668...e-05 -org-element--get-time-properties 1225 0.0306394040 2.501...e-05 -org-get-limited-outline-regexp 2072 0.0140703559 6.790...e-06 -org-element-timestamp-parser 249 0.0124751890 5.010...e-05 -outline-next-heading 847 0.0115396399 1.362...e-05 -org-at-heading-p 2227 0.0109473260 4.915...e-06 -outline-on-heading-p 3452 0.0101107139 2.928...e-06 -string-match 4594 0.0064121759 1.395...e-06 -mapcar 30 0.0041008740 0.0001366958 -#+end_example - -** 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. - -#+BEGIN_SRC elisp - ;; (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)))))))) - -#+END_SRC - -*** :from - -#+BEGIN_SRC elisp - (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"))))) -#+END_SRC - -#+RESULTS: -| 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 - -#+BEGIN_SRC elisp - (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"))))) -#+END_SRC - -#+RESULTS: -| 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 - -#+BEGIN_SRC elisp - (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"))))) -#+END_SRC - -#+RESULTS: -| 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 - -#+BEGIN_SRC elisp - (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))))) -#+END_SRC - -#+RESULTS: -| 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 | - -#+BEGIN_SRC elisp - (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))))) -#+END_SRC - -#+RESULTS: -| 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 -:PROPERTIES: -:TOC: :include descendants :depth 1 -:END: -:CONTENTS: -- [[#github---ndwarshuisorg-sql-sql-backend-for-emacs-org-mode][GitHub - ndwarshuis/org-sql: SQL backend for Emacs Org-Mode]] -- [[#john-kitchin-on-rewriting-the-org-agenda-code][John Kitchin on rewriting the Org agenda code]] -- [[#nicolas-goazious-org-element-cache-implementation][Nicolas Goaziou's org-element cache implementation]] -- [[#uniform-structured-syntax-metaprogramming-and-run-time-compilation][Uniform Structured Syntax, Metaprogramming and Run-time Compilation]] -:END: - -** [[https://github.com/ndwarshuis/org-sql][GitHub - ndwarshuis/org-sql: SQL backend for Emacs Org-Mode]] - -[2020-01-04 Sat 09:03] - -** [[gnus:gmane.emacs.orgmode#CAJ51EToLCm5zDLKu8XeuqEWrLhHZF+OoNkviPSivZbFttzF8=A@mail.gmail.com][John Kitchin on rewriting the Org agenda code]] :Emacs:Org: - -[2019-10-28 Mon 08:28] Originally from [[id:90a68535-2403-4ba9-a117-60be1862628f][this entry in my notes]]. - -#+BEGIN_QUOTE -From: John Kitchin -Subject: Re: [Orgmode] Slow speed of week and month views -Newsgroups: gmane.emacs.orgmode -To: Karl Voit -Cc: "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 -#+END_QUOTE -:PROPERTIES: -:archive.is: http://archive.is/33R9M -:END: - -+ [[https://github.com/m00natic/cl-fdbq][GitHub - m00natic/cl-fdbq: SQL-like operations over fixed field DBs]] - -** 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. - -** [[https://m00natic.github.io/lisp/manual-jit.html][Uniform Structured Syntax, Metaprogramming and Run-time Compilation]] - -* Testing - -[2020-01-08 Wed 07:15] Subtree moved from =tests/data.org=. - -#+BEGIN_SRC elisp - (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)))) -#+END_SRC - -#+BEGIN_SRC elisp -(org-time-string-to-absolute (org-entry-get (point) "SCHEDULED")) -#+END_SRC - - #+BEGIN_SRC elisp :results none - ;; 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)))) - #+END_SRC - -#+BEGIN_SRC elisp :results none - (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)))) -#+END_SRC - -*Note:* Removing tests from here as they're added to =test.el=. - - #+BEGIN_SRC elisp - - (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"))) -#+END_SRC - -#+BEGIN_SRC elisp - - (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))) - -#+END_SRC - -** Agenda censoring - -For sharing screenshots of the agenda without revealing private data. - -#+BEGIN_SRC elisp - (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))))))) -#+END_SRC - -** Agenda examining - -This helps a lot. - -#+BEGIN_SRC elisp - (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))) -#+END_SRC - -** Auto categories - -#+BEGIN_SRC elisp - (let ((org-super-agenda-groups - '((:auto-category t)))) - (org-agenda-list nil nil 'day)) -#+END_SRC - -** Auto grouping - - -#+BEGIN_SRC elisp - (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))) -#+END_SRC - -** Date - -#+BEGIN_SRC elisp - (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))) -#+END_SRC - -** Effort - -#+BEGIN_SRC elisp - (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))) -#+END_SRC - -** Misc - -*** let-plist - -I don't need this right now, but it might come in handy here or elsewhere. - -#+BEGIN_SRC elisp - (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)) -#+END_SRC - -** Profiling - -#+BEGIN_SRC elisp :results none - (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)) -#+END_SRC - - -* [#C] COMMENT Config :noexport: - -** File-local variables - -# Local Variables: -# before-save-hook: ((lambda () (when (fboundp 'unpackaged/org-fix-blank-lines) (unpackaged/org-fix-blank-lines t))) (lambda () (when (fboundp 'ap/org-sort-entries-recursive-multi) (save-excursion (goto-char (point-min)) (ap/org-sort-entries-recursive-multi '(?a ?p ?o))))) org-update-all-dblocks org-make-toc) -# org-ql-ask-unsafe-queries: nil -# End: - From 1a77cb0e1896573417ecca75ceeb09afb8ec888b Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Wed, 18 Nov 2020 08:15:47 -0600 Subject: [PATCH 060/372] Tests: Add more sexp link tests --- tests/test-org-ql.el | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/test-org-ql.el b/tests/test-org-ql.el index 7dad757..d297fc6 100644 --- a/tests/test-org-ql.el +++ b/tests/test-org-ql.el @@ -1514,6 +1514,42 @@ RESULTS should be a list of strings as returned by (expect (var-after-link-save-open 'org-ql-view-query temp-filenames sexp-query) :to-equal sexp-query)))) + (describe "in raw sexp form" + ;; These tests guard against manually constructed queries, which could + ;; theoretically be different than the ones resulting from storing links (i.e. they + ;; might have encoding). These tests probably aren't necessary, but it's good to + ;; be extra careful, because an evil sexp could be harmful if eval'ed. + :var ((raw-link "[[org-ql-search:(todo \"TODO\")]]")) + + (describe "prompt when `org-ql-ask-unsafe-queries' is non-nil" + :var ((org-ql-ask-unsafe-queries t)) + + (it "and signal an error when rejected by user" + (expect (open-link-in raw-link link-buffer "no RET") + :to-throw 'user-error '("Query aborted by user"))) + (it "and run when approved by user" + (expect (open-link-in raw-link link-buffer "yes RET") + :not :to-throw))) + + (it "don't prompt when `org-ql-ask-unsafe-queries' is nil" + (let ((org-ql-ask-unsafe-queries nil)) + (expect (open-link-in raw-link link-buffer "no RET") + :not :to-throw))) + + (it "match after restoring" + (let ((org-ql-ask-unsafe-queries nil) ; Disable safety check for this test. + (org-ql-view-buffer view-buffer)) + (expect (open-link-in raw-link link-buffer "no RET") + :not :to-throw) + (expect (buffer-local-value 'org-ql-view-query view-buffer) + :to-equal '(todo "TODO")))) + + (it "could be evil when not prompted about" + ;; This test confirms what *could* happen if these checks weren't in place. + (let ((org-ql-ask-unsafe-queries nil)) + (expect (open-link-in "[[org-ql-search:(error \"EVIL!\")]]" link-buffer nil) + :to-throw 'error '("EVIL!"))))) + (describe "in string form" (it "match after restoring" ;; NOTE: This test includes the string query being replaced with its sexp form after the query is run. From 0f5cf5eff320cd718d60359d4562430955719040 Mon Sep 17 00:00:00 2001 From: Ihor Radchenko Date: Sun, 12 Apr 2020 13:40:09 +0800 Subject: [PATCH 061/372] Honor restriction when it is set from inside agenda dispatcher --- org-ql-search.el | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/org-ql-search.el b/org-ql-search.el index cf49281..9f7ba3f 100644 --- a/org-ql-search.el +++ b/org-ql-search.el @@ -233,15 +233,14 @@ If `org-ql-block-header' is non-nil, it is used as the header string for the block, otherwise a the header is formed automatically from the query." (let (narrow-p old-beg old-end) - (when-let* ((from (pcase org-agenda-overriding-restriction + (when-let* ((from (pcase org-agenda-restrict ('nil (org-agenda-files nil 'ifmode)) - ('file (get 'org-agenda-files 'org-restrict)) - ('subtree (prog1 org-agenda-restrict - (with-current-buffer org-agenda-restrict - ;; Narrow the buffer; remember to widen it later. - (setf old-beg (point-min) old-end (point-max) - narrow-p t) - (narrow-to-region org-agenda-restrict-begin org-agenda-restrict-end)))))) + (_ (prog1 org-agenda-restrict + (with-current-buffer org-agenda-restrict + ;; Narrow the buffer; remember to widen it later. + (setf old-beg (point-min) old-end (point-max) + narrow-p t) + (narrow-to-region org-agenda-restrict-begin org-agenda-restrict-end)))))) (items (org-ql-select from query :action 'element-with-markers :narrow narrow-p))) From 038c5ae3bf5e9077989e65c186f3da8e3034de56 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Fri, 20 Nov 2020 07:54:44 -0600 Subject: [PATCH 062/372] Update changelog, version headers Released as 0.4.9. --- README.org | 5 ++ org-ql.el | 2 +- org-ql.info | 149 +++++++++++++++++++++++++++++++--------------------- 3 files changed, 95 insertions(+), 61 deletions(-) diff --git a/README.org b/README.org index 9b6b5d7..68df9aa 100644 --- a/README.org +++ b/README.org @@ -398,6 +398,11 @@ Expands into a call to ~org-ql-select~ with the same arguments. For convenience /Note:/ Breaking changes may be made before version 1.0, but in the event of major changes, attempts at backward compatibility will be made with obsolescence declarations, translation of arguments, etc. Users who need stability guarantees before 1.0 may choose to use tagged stable releases. +** 0.4.9 + +*Fixed* ++ Agenda restriction in =org-ql-block=. (Fixes [[https://github.com/alphapapa/org-ql/issues/84][#84]]. Thanks to [[https://github.com/yantar92][Ihor Radchenko]].) + ** 0.4.8 *Fixed* diff --git a/org-ql.el b/org-ql.el index 85b47cd..2fd64fa 100644 --- a/org-ql.el +++ b/org-ql.el @@ -2,7 +2,7 @@ ;; Author: Adam Porter ;; Url: https://github.com/alphapapa/org-ql -;; Version: 0.4.8 +;; Version: 0.4.9 ;; Package-Requires: ((emacs "26.1") (dash "2.13") (dash-functional "1.2.0") (f "0.17.2") (org "9.0") (org-super-agenda "1.2-pre") (ov "1.0.6") (peg "0.6") (s "1.12.0") (ts "0.2-pre")) ;; Keywords: hypermedia, outlines, Org, agenda diff --git a/org-ql.info b/org-ql.info index 6e8a77d..a6e4465 100644 --- a/org-ql.info +++ b/org-ql.info @@ -63,6 +63,8 @@ Functions / Macros Changelog +* 0.4.9: 049. +* 0.4.8: 048. * 0.4.7: 047. * 0.4.6: 046. * 0.4.5: 045. @@ -710,6 +712,8 @@ releases. * Menu: +* 0.4.9: 049. +* 0.4.8: 048. * 0.4.7: 047. * 0.4.6: 046. * 0.4.5: 045. @@ -728,9 +732,32 @@ releases. * 0.1: 01.  -File: README.info, Node: 047, Next: 046, Up: Changelog +File: README.info, Node: 049, Next: 048, Up: Changelog -5.1 0.4.7 +5.1 0.4.9 +========= + +*Fixed* + • Agenda restriction in org-ql-block. (Fixes #84 + (https://github.com/alphapapa/org-ql/issues/84). Thanks to Ihor + Radchenko (https://github.com/yantar92).) + + +File: README.info, Node: 048, Next: 047, Prev: 049, Up: Changelog + +5.2 0.4.8 +========= + +*Fixed* + • Multiple sorters not preserved when refreshing views. (Fixes #136 + (https://github.com/alphapapa/org-ql/issues/136), #137 + (https://github.com/alphapapa/org-ql/pull/137). Thanks to Imran + Khan (https://github.com/natrys).) + + +File: README.info, Node: 047, Next: 046, Prev: 048, Up: Changelog + +5.3 0.4.7 ========= *Fixed* @@ -743,7 +770,7 @@ File: README.info, Node: 047, Next: 046, Up: Changelog  File: README.info, Node: 046, Next: 045, Prev: 047, Up: Changelog -5.2 0.4.6 +5.4 0.4.6 ========= *Fixed* @@ -756,7 +783,7 @@ File: README.info, Node: 046, Next: 045, Prev: 047, Up: Changelog  File: README.info, Node: 045, Next: 044, Prev: 046, Up: Changelog -5.3 0.4.5 +5.5 0.4.5 ========= *Fixed* @@ -768,7 +795,7 @@ File: README.info, Node: 045, Next: 044, Prev: 046, Up: Changelog  File: README.info, Node: 044, Next: 043, Prev: 045, Up: Changelog -5.4 0.4.4 +5.6 0.4.4 ========= *Fixed* @@ -780,7 +807,7 @@ File: README.info, Node: 044, Next: 043, Prev: 045, Up: Changelog  File: README.info, Node: 043, Next: 042, Prev: 044, Up: Changelog -5.5 0.4.3 +5.7 0.4.3 ========= *Fixed* @@ -790,7 +817,7 @@ File: README.info, Node: 043, Next: 042, Prev: 044, Up: Changelog  File: README.info, Node: 042, Next: 041, Prev: 043, Up: Changelog -5.6 0.4.2 +5.8 0.4.2 ========= *Fixed* @@ -799,7 +826,7 @@ File: README.info, Node: 042, Next: 041, Prev: 043, Up: Changelog  File: README.info, Node: 041, Next: 04, Prev: 042, Up: Changelog -5.7 0.4.1 +5.9 0.4.1 ========= *Fixed* @@ -809,8 +836,8 @@ File: README.info, Node: 041, Next: 04, Prev: 042, Up: Changelog  File: README.info, Node: 04, Next: 032, Prev: 041, Up: Changelog -5.8 0.4 -======= +5.10 0.4 +======== _Note:_ The next release, 0.5, may include changes which will require minor updates to written queries (e.g. a few predicates may be @@ -890,8 +917,8 @@ as they will be pushed to the master branch when ready.  File: README.info, Node: 032, Next: 031, Prev: 04, Up: Changelog -5.9 0.3.2 -========= +5.11 0.3.2 +========== *Fixed* • In org-ql-search, accept symbol as ‘:super-groups’ argument. @@ -903,7 +930,7 @@ File: README.info, Node: 032, Next: 031, Prev: 04, Up: Changelog  File: README.info, Node: 031, Next: 03, Prev: 032, Up: Changelog -5.10 0.3.1 +5.12 0.3.1 ========== *Fixed* @@ -913,7 +940,7 @@ File: README.info, Node: 031, Next: 03, Prev: 032, Up: Changelog  File: README.info, Node: 03, Next: 023, Prev: 031, Up: Changelog -5.11 0.3 +5.13 0.3 ======== *Added* @@ -978,7 +1005,7 @@ File: README.info, Node: 03, Next: 023, Prev: 031, Up: Changelog  File: README.info, Node: 023, Next: 022, Prev: 03, Up: Changelog -5.12 0.2.3 +5.14 0.2.3 ========== *Fixed* @@ -988,7 +1015,7 @@ File: README.info, Node: 023, Next: 022, Prev: 03, Up: Changelog  File: README.info, Node: 022, Next: 021, Prev: 023, Up: Changelog -5.13 0.2.2 +5.15 0.2.2 ========== *Fixed* @@ -999,7 +1026,7 @@ File: README.info, Node: 022, Next: 021, Prev: 023, Up: Changelog  File: README.info, Node: 021, Next: 02, Prev: 022, Up: Changelog -5.14 0.2.1 +5.16 0.2.1 ========== *Fixed* @@ -1009,7 +1036,7 @@ File: README.info, Node: 021, Next: 02, Prev: 022, Up: Changelog  File: README.info, Node: 02, Next: 01, Prev: 021, Up: Changelog -5.15 0.2 +5.17 0.2 ======== *Added* @@ -1092,7 +1119,7 @@ File: README.info, Node: 02, Next: 01, Prev: 021, Up: Changelog  File: README.info, Node: 01, Prev: 02, Up: Changelog -5.16 0.1 +5.18 0.1 ======== First tagged release. @@ -1153,47 +1180,49 @@ GPLv3  Tag Table: Node: Top225 -Node: Contents1507 -Node: Screenshots1681 -Node: Installation1799 -Node: Quelpa2437 -Node: Usage2880 -Node: Commands3229 -Node: org-ql-search3702 -Node: helm-org-ql5350 -Node: org-ql-view5762 -Node: org-ql-view-sidebar5960 -Node: org-ql-view-recent-items6316 -Node: org-ql-sparse-tree6800 -Node: Queries7600 -Node: Non-sexp query syntax8508 -Node: General predicates10215 -Node: Ancestor/descendant predicates15022 -Node: Date/time predicates16150 -Node: Functions / Macros18805 -Node: Agenda-like views19038 -Node: Listing / acting-on results20443 -Node: Changelog25045 -Node: 04725679 -Node: 04626062 -Node: 04526462 -Node: 04426821 -Node: 04327178 -Node: 04227373 -Node: 04127532 -Node: 0427771 -Node: 03231702 -Node: 03132079 -Node: 0332276 -Node: 02335251 -Node: 02235479 -Node: 02135747 -Node: 0235946 -Node: 0139981 -Node: Notes40082 -Node: Comparison with Org Agenda searches40244 -Node: org-sidebar41115 -Node: License41394 +Node: Contents1537 +Node: Screenshots1711 +Node: Installation1829 +Node: Quelpa2467 +Node: Usage2910 +Node: Commands3259 +Node: org-ql-search3732 +Node: helm-org-ql5380 +Node: org-ql-view5792 +Node: org-ql-view-sidebar5990 +Node: org-ql-view-recent-items6346 +Node: org-ql-sparse-tree6830 +Node: Queries7630 +Node: Non-sexp query syntax8538 +Node: General predicates10245 +Node: Ancestor/descendant predicates15052 +Node: Date/time predicates16180 +Node: Functions / Macros18835 +Node: Agenda-like views19068 +Node: Listing / acting-on results20473 +Node: Changelog25075 +Node: 04925739 +Node: 04826002 +Node: 04726349 +Node: 04626744 +Node: 04527144 +Node: 04427503 +Node: 04327860 +Node: 04228055 +Node: 04128214 +Node: 0428453 +Node: 03232386 +Node: 03132765 +Node: 0332962 +Node: 02335937 +Node: 02236165 +Node: 02136433 +Node: 0236632 +Node: 0140667 +Node: Notes40768 +Node: Comparison with Org Agenda searches40930 +Node: org-sidebar41801 +Node: License42080  End Tag Table From e4fb1cce2e7f41eac521fccf1367c6454b5d1154 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Fri, 20 Nov 2020 08:13:15 -0600 Subject: [PATCH 063/372] Docs: Explain how caching works Fixes #132. Thanks to Mikhail Skorzhinskii (@mskorzhinskiy) for suggesting. --- README.org | 6 +++++ org-ql.info | 73 +++++++++++++++++++++++++++++++---------------------- 2 files changed, 49 insertions(+), 30 deletions(-) diff --git a/README.org b/README.org index 0ff4d89..741e745 100644 --- a/README.org +++ b/README.org @@ -299,6 +299,12 @@ The variable =org-ql-block-header= may be bound to a string to use as the block *** Listing / acting-on results +**** Caching + +Org QL uses a per-buffer cache to speed up subsequent searches. It's keyed on query expressions, which means that, for the same query in the same buffer, if the buffer has not been modified since the last time the query was run, the cached result will be returned, and the query will not be evaluated in that buffer again. + +Therefore, since query expressions are not guaranteed to be evaluated when these functions are called, they should be free of side effects. Any side effects should be done in the action expression instead. + **** Function: ~org-ql-select~ /Arguments:/ ~(buffers-or-files query &key action narrow sort)~ diff --git a/org-ql.info b/org-ql.info index 5c9bb45..519f605 100644 --- a/org-ql.info +++ b/org-ql.info @@ -607,7 +607,20 @@ File: README.info, Node: Listing / acting-on results, Prev: Agenda-like views, 4.3.2 Listing / acting-on results --------------------------------- - 1. Function: ‘org-ql-select’ + 1. Caching + + Org QL uses a per-buffer cache to speed up subsequent searches. + It’s keyed on query expressions, which means that, for the same + query in the same buffer, if the buffer has not been modified since + the last time the query was run, the cached result will be + returned, and the query will not be evaluated in that buffer again. + + Therefore, since query expressions are not guaranteed to be + evaluated when these functions are called, they should be free of + side effects. Any side effects should be done in the action + expression instead. + + 2. Function: ‘org-ql-select’ _Arguments:_ ‘(buffers-or-files query &key action narrow sort)’ @@ -667,7 +680,7 @@ File: README.info, Node: Listing / acting-on results, Prev: Agenda-like views, '(todo "WAITING")) ;; => ((headline (:raw-value "Visit the moon" ...) ...) ...) - 2. Function: ‘org-ql-query’ + 3. Function: ‘org-ql-query’ _Arguments:_ ‘(&key (select 'element-with-markers) from where order-by narrow)’ @@ -712,7 +725,7 @@ File: README.info, Node: Listing / acting-on results, Prev: Agenda-like views, :where '(todo "WAITING")) ;; => ((headline (:raw-value "Visit the moon" ...) ...) ...) - 3. Macro: ‘org-ql’ + 4. Macro: ‘org-ql’ _Arguments:_ ‘(buffers-or-files query &key sort narrow markers action)’ @@ -1336,33 +1349,33 @@ Node: Date/time predicates17145 Node: Functions / Macros19800 Node: Agenda-like views20055 Node: Listing / acting-on results21460 -Node: Dynamic block26062 -Node: Links28760 -Node: Tips29447 -Node: Changelog29765 -Node: 05-pre30449 -Node: 04931688 -Node: 04831966 -Node: 04732313 -Node: 04632708 -Node: 04533108 -Node: 04433467 -Node: 04333824 -Node: 04234019 -Node: 04134178 -Node: 0434419 -Node: 03238352 -Node: 03138731 -Node: 0338928 -Node: 02341903 -Node: 02242131 -Node: 02142399 -Node: 0242598 -Node: 0146633 -Node: Notes46734 -Node: Comparison with Org Agenda searches46896 -Node: org-sidebar47768 -Node: License48047 +Node: Dynamic block26655 +Node: Links29353 +Node: Tips30040 +Node: Changelog30358 +Node: 05-pre31042 +Node: 04932281 +Node: 04832559 +Node: 04732906 +Node: 04633301 +Node: 04533701 +Node: 04434060 +Node: 04334417 +Node: 04234612 +Node: 04134771 +Node: 0435012 +Node: 03238945 +Node: 03139324 +Node: 0339521 +Node: 02342496 +Node: 02242724 +Node: 02142992 +Node: 0243191 +Node: 0147226 +Node: Notes47327 +Node: Comparison with Org Agenda searches47489 +Node: org-sidebar48361 +Node: License48640  End Tag Table From 6bb2db93921e70293fb2f73f6cfe51811a9b28db Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Fri, 20 Nov 2020 08:22:41 -0600 Subject: [PATCH 064/372] Docs: Improve query explanation See #92. --- README.org | 2 +- org-ql.info | 79 +++++++++++++++++++++++++++-------------------------- 2 files changed, 42 insertions(+), 39 deletions(-) diff --git a/README.org b/README.org index 741e745..2d7f300 100644 --- a/README.org +++ b/README.org @@ -165,7 +165,7 @@ Show a sparse tree for ~QUERY~ in ~BUFFER~ and return number of results. The tr - [[#datetime-predicates][Date/time predicates]] :END: -An =org-ql= query is a lisp form which may contain arbitrary lisp forms, as well as certain built-in predicates. It is byte-compiled into a predicate function which is tested with point on each heading in an Org buffer; when it returns non-nil, the heading matches the query. +An =org-ql= query is a Lisp expression which may contain arbitrary expressions, as well as calling certain built-in predicates. It is byte-compiled into a predicate function which is tested with point on each heading in an Org buffer; when it returns non-nil, the heading matches the query. When possible, certain built-in predicates are optimized away to whole-buffer regular expression searches, which are much faster to search for than testing the predicate on each heading. *Notes:* + Bare strings like ~"string"~ are automatically converted to ~(regexp "string")~ predicates. diff --git a/org-ql.info b/org-ql.info index 519f605..da41319 100644 --- a/org-ql.info +++ b/org-ql.info @@ -318,10 +318,13 @@ File: README.info, Node: Queries, Next: Functions / Macros, Prev: Commands, =========== • • • • - An org-ql query is a lisp form which may contain arbitrary lisp -forms, as well as certain built-in predicates. It is byte-compiled into -a predicate function which is tested with point on each heading in an -Org buffer; when it returns non-nil, the heading matches the query. + An org-ql query is a Lisp expression which may contain arbitrary +expressions, as well as calling certain built-in predicates. It is +byte-compiled into a predicate function which is tested with point on +each heading in an Org buffer; when it returns non-nil, the heading +matches the query. When possible, certain built-in predicates are +optimized away to whole-buffer regular expression searches, which are +much faster to search for than testing the predicate on each heading. *Notes:* • Bare strings like ‘"string"’ are automatically converted to @@ -1342,40 +1345,40 @@ Node: org-ql-view-sidebar6349 Node: org-ql-view-recent-items6705 Node: org-ql-sparse-tree7189 Node: Queries7989 -Node: Non-sexp query syntax8897 -Node: General predicates10604 -Node: Ancestor/descendant predicates16017 -Node: Date/time predicates17145 -Node: Functions / Macros19800 -Node: Agenda-like views20055 -Node: Listing / acting-on results21460 -Node: Dynamic block26655 -Node: Links29353 -Node: Tips30040 -Node: Changelog30358 -Node: 05-pre31042 -Node: 04932281 -Node: 04832559 -Node: 04732906 -Node: 04633301 -Node: 04533701 -Node: 04434060 -Node: 04334417 -Node: 04234612 -Node: 04134771 -Node: 0435012 -Node: 03238945 -Node: 03139324 -Node: 0339521 -Node: 02342496 -Node: 02242724 -Node: 02142992 -Node: 0243191 -Node: 0147226 -Node: Notes47327 -Node: Comparison with Org Agenda searches47489 -Node: org-sidebar48361 -Node: License48640 +Node: Non-sexp query syntax9100 +Node: General predicates10807 +Node: Ancestor/descendant predicates16220 +Node: Date/time predicates17348 +Node: Functions / Macros20003 +Node: Agenda-like views20258 +Node: Listing / acting-on results21663 +Node: Dynamic block26858 +Node: Links29556 +Node: Tips30243 +Node: Changelog30561 +Node: 05-pre31245 +Node: 04932484 +Node: 04832762 +Node: 04733109 +Node: 04633504 +Node: 04533904 +Node: 04434263 +Node: 04334620 +Node: 04234815 +Node: 04134974 +Node: 0435215 +Node: 03239148 +Node: 03139527 +Node: 0339724 +Node: 02342699 +Node: 02242927 +Node: 02143195 +Node: 0243394 +Node: 0147429 +Node: Notes47530 +Node: Comparison with Org Agenda searches47692 +Node: org-sidebar48564 +Node: License48843  End Tag Table From 2aa8e84fc999cd1e90f9ae0008dfbe7865876f5a Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Sun, 22 Nov 2020 17:09:36 -0600 Subject: [PATCH 065/372] Fix: peg-related compiler warnings --- org-ql.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/org-ql.el b/org-ql.el index 9dad848..ccf63fd 100644 --- a/org-ql.el +++ b/org-ql.el @@ -1731,8 +1731,8 @@ element should be a regexp string." ;; Fix compiler warnings probably caused by `peg' not using lexical-binding. ;; TODO: File bug report upstream. -(defvar peg-errors nil) -(defvar peg-stack nil) +(defvar peg-errors) +(defvar peg-stack) (defmacro org-ql--peg-parse-string (rules string &optional noerror) "Parse STRING according to RULES." From 46ad26f1a02b7a6f3d0badbe9a148def7c0d914a Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Sun, 22 Nov 2020 17:20:12 -0600 Subject: [PATCH 066/372] Change: Require org-super-agenda 1.2 Which includes the safety checks we use for links. --- org-ql.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/org-ql.el b/org-ql.el index ccf63fd..f3d89e7 100644 --- a/org-ql.el +++ b/org-ql.el @@ -3,7 +3,7 @@ ;; Author: Adam Porter ;; Url: https://github.com/alphapapa/org-ql ;; Version: 0.5-pre -;; Package-Requires: ((emacs "26.1") (dash "2.13") (dash-functional "1.2.0") (f "0.17.2") (map "2.1") (org "9.0") (org-super-agenda "1.2-pre") (ov "1.0.6") (peg "0.6") (s "1.12.0") (transient "0.1") (ts "0.2-pre")) +;; Package-Requires: ((emacs "26.1") (dash "2.13") (dash-functional "1.2.0") (f "0.17.2") (map "2.1") (org "9.0") (org-super-agenda "1.2") (ov "1.0.6") (peg "0.6") (s "1.12.0") (transient "0.1") (ts "0.2-pre")) ;; Keywords: hypermedia, outlines, Org, agenda ;;; Commentary: From c41cd4be822434ba360540e563a5b9f379886b85 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Sun, 22 Nov 2020 17:25:04 -0600 Subject: [PATCH 067/372] Change: (-view--link-open, -view--link-follow) Rename function For consistency with Org's naming scheme. --- org-ql-view.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/org-ql-view.el b/org-ql-view.el index 089baf6..0bc1b97 100644 --- a/org-ql-view.el +++ b/org-ql-view.el @@ -578,7 +578,7 @@ dates in the past, and negative for dates in the future." ;; than `org-ql-view' saved views, the link type is "org-ql-search". (org-link-set-parameters "org-ql-search" - :follow #'org-ql-view--link-open + :follow #'org-ql-view--link-follow :store #'org-ql-view--link-store) ;; We require the URL libraries in the functions to hopefully avoid @@ -588,7 +588,7 @@ dates in the past, and negative for dates in the future." (require 'url-parse) (require 'url-util)) -(defun org-ql-view--link-open (path) +(defun org-ql-view--link-follow (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-view--link-store'." From 7f8328a87c8c9fdacb4458243a0a9338206db0bd Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Sun, 22 Nov 2020 17:25:59 -0600 Subject: [PATCH 068/372] Change: (-view--link-follow) Ignore second argument Hopefully this allows compatibility with Org 9.4, since Org's news file doesn't say what the second argument is. --- org-ql-view.el | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/org-ql-view.el b/org-ql-view.el index 0bc1b97..b954fc0 100644 --- a/org-ql-view.el +++ b/org-ql-view.el @@ -588,10 +588,13 @@ dates in the past, and negative for dates in the future." (require 'url-parse) (require 'url-util)) -(defun org-ql-view--link-follow (path) +(defun org-ql-view--link-follow (path &optional _ignored) "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-view--link-store'." +protocol. See, e.g. `org-ql-view--link-store'. + +The optional, second argument is temporarily _IGNORED for +purposes of compatibility with changes in Org 9.4." (require 'url-parse) (require 'url-util) (when (version<= "9.3" (org-version)) From 56a08208e571b9e23e90110909fcc5937e1997c9 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Sun, 22 Nov 2020 17:31:15 -0600 Subject: [PATCH 069/372] Change: (defgroup org-ql) Add Info manual link --- org-ql.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/org-ql.el b/org-ql.el index f3d89e7..1ef99fe 100644 --- a/org-ql.el +++ b/org-ql.el @@ -134,7 +134,7 @@ This list should not contain any duplicates.")) (defgroup org-ql nil "Customization for `org-ql'." :group 'org - ;; TODO: Add info manual link. + :link '(custom-manual "(org-ql)Usage") :link '(url-link "https://github.com/alphapapa/org-ql")) (defcustom org-ql-ask-unsafe-queries t From 90f712aba9ea386d7bb65f9300ddc4527c1d000d Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Sun, 22 Nov 2020 17:33:46 -0600 Subject: [PATCH 070/372] Change: (org-ql) Declare macro obsolete --- README.org | 3 + org-ql.el | 4 +- org-ql.info | 50 ++--- tests/test-org-ql.el | 437 ++++++++++++++++++++++--------------------- 4 files changed, 252 insertions(+), 242 deletions(-) diff --git a/README.org b/README.org index 2d7f300..fed38c4 100644 --- a/README.org +++ b/README.org @@ -478,6 +478,9 @@ Simple links may also be written manually in either sexp or non-sexp form, like: *Internal* + When formatting entries for Org QL View buffers, use internal function for retrieving heading tags. This improves speed by using our cache, and it removes the need for a compatibility alias for Org versions before 9.3. +*Deprecated* ++ Macro =org-ql= is marked obsolete. It will be removed in v0.7. Functions =org-ql-select= and =org-ql-query= should be used instead. (The macro serves only to confuse with regard to quoting arguments.) + *Acknowledgments* + [[https://github.com/tpeacock19][tpeacock19]] for extensive help testing new features in this version. diff --git a/org-ql.el b/org-ql.el index 1ef99fe..3e38088 100644 --- a/org-ql.el +++ b/org-ql.el @@ -184,12 +184,12 @@ match." (push (list :name ',pred-name :aliases ',aliases :fn ',fn-name :docstring ,docstring :args ',args) org-ql-predicates)) (cl-defun ,fn-name ,args ,docstring ,@body)))) -;; TODO: Mark as obsolete/deprecated. ;;;###autoload (cl-defmacro org-ql (buffers-or-files query &key sort narrow action) "Expands into a call to `org-ql-select' with the same arguments. For convenience, arguments should be unquoted." - (declare (indent defun)) + (declare (indent defun) + (obsolete "Please use functions `org-ql-select' or `org-ql-query' instead" "org-ql 0.5")) `(org-ql-select ,buffers-or-files ',query :action ',action diff --git a/org-ql.info b/org-ql.info index da41319..3e8c933 100644 --- a/org-ql.info +++ b/org-ql.info @@ -882,6 +882,12 @@ File: README.info, Node: 05-pre, Next: 049, Up: Changelog our cache, and it removes the need for a compatibility alias for Org versions before 9.3. + *Deprecated* + • Macro org-ql is marked obsolete. It will be removed in v0.7. + Functions org-ql-select and org-ql-query should be used instead. + (The macro serves only to confuse with regard to quoting + arguments.) + *Acknowledgments* • tpeacock19 (https://github.com/tpeacock19) for extensive help testing new features in this version. @@ -1357,28 +1363,28 @@ Node: Links29556 Node: Tips30243 Node: Changelog30561 Node: 05-pre31245 -Node: 04932484 -Node: 04832762 -Node: 04733109 -Node: 04633504 -Node: 04533904 -Node: 04434263 -Node: 04334620 -Node: 04234815 -Node: 04134974 -Node: 0435215 -Node: 03239148 -Node: 03139527 -Node: 0339724 -Node: 02342699 -Node: 02242927 -Node: 02143195 -Node: 0243394 -Node: 0147429 -Node: Notes47530 -Node: Comparison with Org Agenda searches47692 -Node: org-sidebar48564 -Node: License48843 +Node: 04932719 +Node: 04832997 +Node: 04733344 +Node: 04633739 +Node: 04534139 +Node: 04434498 +Node: 04334855 +Node: 04235050 +Node: 04135209 +Node: 0435450 +Node: 03239383 +Node: 03139762 +Node: 0339959 +Node: 02342934 +Node: 02243162 +Node: 02143430 +Node: 0243629 +Node: 0147664 +Node: Notes47765 +Node: Comparison with Org Agenda searches47927 +Node: org-sidebar48799 +Node: License49078  End Tag Table diff --git a/tests/test-org-ql.el b/tests/test-org-ql.el index d297fc6..c9d560e 100644 --- a/tests/test-org-ql.el +++ b/tests/test-org-ql.el @@ -54,14 +54,14 @@ Set at runtime by test suite.") (_ nil))) (result (pcase sexp (`(org-ql-expect ,args) - (org-ql-test--format-result--ql `(org-ql org-ql-test-buffer + (org-ql-test--format-result--ql `(org-ql-select org-ql-test-buffer ,@args :action (org-ql-test-org-get-heading)))) (`(org-ql-expect ,args ,_ :buffer ,buffer) - (org-ql-test--format-result--ql `(org-ql ,buffer + (org-ql-test--format-result--ql `(org-ql-select ,buffer ,@args :action (org-ql-test-org-get-heading)))) - (`(org-ql . _) (org-ql-test--format-result--ql sexp)) + (`(org-ql-select . _) (org-ql-test--format-result--ql sexp)) (`(org-ql--query-preamble . _) (org-ql-test--format-result--query-preamble sexp)) (`(org-ql--pre-process-query . _) (format "'%S" (eval sexp))) (_ nil)))) @@ -92,7 +92,7 @@ Set at runtime by test suite.") (interactive) (if-let* ((sexp (elisp--preceding-sexp)) (sexp (pcase sexp - (`(org-ql . ,_) (setf (car sexp) 'org-ql-agenda)) + (`(org-ql-select . ,_) (setf (car sexp) 'org-ql-agenda)) (`(org-ql-expect ,ql-args . ,_) (setf sexp `(org-ql-search org-ql-test-buffer ',@ql-args))) (_ nil)))) @@ -131,9 +131,9 @@ Based on Buttercup macro `it'." RESULTS should be a list of strings as returned by `org-ql-test-org-get-heading'." (declare (indent defun)) - `(expect (org-ql ,buffer + `(expect (org-ql-select ,buffer ,@ql-args - :action (org-ql-test-org-get-heading)) + :action '(org-ql-test-org-get-heading)) :to-equal ,results)) (defmacro org-ql-then (&rest body) @@ -201,6 +201,7 @@ RESULTS should be a list of strings as returned by (describe "Query functions/macros" (it "org-ql" + ;; FIXME: Remove when org-ql macro is removed. (expect (length (org-ql org-ql-test-buffer (category) :sort deadline)) @@ -422,423 +423,423 @@ RESULTS should be a list of strings as returned by (describe "(ancestors)" (org-ql-it "without sub-query" - (org-ql-expect ((ancestors)) + (org-ql-expect ('(ancestors)) '("Take over the world" "Skype with president of Antarctica" "Take over Mars" "Visit Mars" "Take over the moon" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "/r/emacs" "Shop for groceries" "Sunrise/sunset" "Rewrite Emacs in Common Lisp" "Write a symphony"))) (org-ql-it "with sub-query" - (org-ql-expect ((ancestors (heading "universe"))) + (org-ql-expect ('(ancestors (heading "universe"))) '("Take over the world" "Skype with president of Antarctica" "Take over Mars" "Visit Mars" "Take over the moon" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language")))) (describe "(parent)" (org-ql-it "without sub-query" - (org-ql-expect ((parent)) + (org-ql-expect ('(parent)) '("Take over the world" "Skype with president of Antarctica" "Take over Mars" "Visit Mars" "Take over the moon" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "/r/emacs" "Shop for groceries" "Sunrise/sunset" "Rewrite Emacs in Common Lisp" "Write a symphony"))) (org-ql-it "with sub-query" - (org-ql-expect ((parent (and (todo) (priority "A")))) + (org-ql-expect ('(parent (and (todo) (priority "A")))) '("Take over the world" "Skype with president of Antarctica" "Take over Mars" "Take over the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language")))) (describe "(category)" (org-ql-it "without arguments" - (expect (length (org-ql org-ql-test-buffer - (category))) + (expect (length (org-ql-select org-ql-test-buffer + '(category))) :to-equal org-ql-test-num-headings)) (org-ql-it "with a category" - (org-ql-expect ((category "ambition")) + (org-ql-expect ('(category "ambition")) '("Take over the universe" "Take over the world" "Skype with president of Antarctica" "Take over Mars" "Visit Mars" "Take over the moon" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language")))) (describe "(children)" (org-ql-it "without arguments" - (org-ql-expect ((children)) + (org-ql-expect ('(children)) '("Take over the universe" "Take over the world" "Take over Mars" "Take over the moon" "Recurring" "Ideas"))) (org-ql-it "with sub-query" - (org-ql-expect ((children (todo "CHECK"))) + (org-ql-expect ('(children (todo "CHECK"))) '("Recurring"))) (org-ql-it "with grandchildren query" ;; It's really cool how this works. It's so simple. - (org-ql-expect ((children (children "moon"))) + (org-ql-expect ('(children (children "moon"))) '("Take over the universe")))) (describe "(descendants)" (org-ql-it "without arguments" - (org-ql-expect ((descendants)) + (org-ql-expect ('(descendants)) '("Take over the universe" "Take over the world" "Take over Mars" "Take over the moon" "Recurring" "Ideas"))) (org-ql-it "with sub-query" - (org-ql-expect ((descendants (todo "CHECK"))) + (org-ql-expect ('(descendants (todo "CHECK"))) '("Recurring"))) (org-ql-it "with granddescendants query" - (org-ql-expect ((descendants (descendants "moon"))) + (org-ql-expect ('(descendants (descendants "moon"))) '("Take over the universe"))) (org-ql-it "with query that should not match parent" ;; This test would fail if the `descendants' predicate did not properly exclude ;; the parent heading by narrowing the buffer to begin at the first child. - (org-ql-expect ((and (descendants (todo "WAITING")) - (not (descendants (todo "TODO" "NEXT"))))) + (org-ql-expect ('(and (descendants (todo "WAITING")) + (not (descendants (todo "TODO" "NEXT"))))) '("Take over the moon")))) (describe "(clocked)" (org-ql-it "without arguments" - (org-ql-expect ((clocked)) + (org-ql-expect ('(clocked)) '("Learn universal sign language"))) (org-ql-it "with a number" (org-ql-then - (org-ql-expect ((clocked 10)) + (org-ql-expect ('(clocked 10)) '("Learn universal sign language")))) (org-ql-it ":from a timestamp" - (org-ql-expect ((clocked :from "2017-07-05")) + (org-ql-expect ('(clocked :from "2017-07-05")) '("Learn universal sign language")) - (org-ql-expect ((clocked :from "2017-07-06")) + (org-ql-expect ('(clocked :from "2017-07-06")) nil)) (org-ql-it ":from today" (org-ql-then - (org-ql-expect ((clocked :from today)) + (org-ql-expect ('(clocked :from today)) '("Learn universal sign language")))) (org-ql-it ":to a timestamp" - (org-ql-expect ((clocked :to "2017-07-05")) + (org-ql-expect ('(clocked :to "2017-07-05")) '("Learn universal sign language")) - (org-ql-expect ((clocked :to "2017-07-04")) + (org-ql-expect ('(clocked :to "2017-07-04")) nil)) (org-ql-it ":to today" (org-ql-then - (org-ql-expect ((clocked :to today)) + (org-ql-expect ('(clocked :to today)) '("Learn universal sign language")))) (org-ql-it ":on a date" - (org-ql-expect ((clocked :on "2017-07-05")) + (org-ql-expect ('(clocked :on "2017-07-05")) '("Learn universal sign language")) - (org-ql-expect ((clocked :on "2018-12-02")) + (org-ql-expect ('(clocked :on "2018-12-02")) nil)) (org-ql-it ":on today" (org-ql-then - (org-ql-expect ((clocked :on today)) + (org-ql-expect ('(clocked :on today)) '("Learn universal sign language")))) (org-ql-it "within a range (:from and :to)" - (org-ql-expect ((clocked :from "2017-07-04" :to "2018-12-11")) + (org-ql-expect ('(clocked :from "2017-07-04" :to "2018-12-11")) '("Learn universal sign language")) - (org-ql-expect ((clocked :from "2017-07-06" :to "2018-12-11")) + (org-ql-expect ('(clocked :from "2017-07-06" :to "2018-12-11")) nil) - (org-ql-expect ((clocked :from "2017-07-01" :to "2017-07-04")) + (org-ql-expect ('(clocked :from "2017-07-01" :to "2017-07-04")) nil))) (describe "(closed)" (org-ql-it "without arguments" - (org-ql-expect ((closed)) + (org-ql-expect ('(closed)) '("Learn universal sign language"))) (org-ql-it "with a number" (org-ql-then - (org-ql-expect ((closed 10)) + (org-ql-expect ('(closed 10)) '("Learn universal sign language")))) (org-ql-it ":on" - (org-ql-expect ((closed :on "2017-07-05")) + (org-ql-expect ('(closed :on "2017-07-05")) '("Learn universal sign language")) (org-ql-then - (org-ql-expect ((closed :on today)) + (org-ql-expect ('(closed :on today)) '("Learn universal sign language"))) - (org-ql-expect ((closed :on "2019-06-09")) + (org-ql-expect ('(closed :on "2019-06-09")) nil)) (org-ql-it ":from" - (org-ql-expect ((closed :from "2017-07-04")) + (org-ql-expect ('(closed :from "2017-07-04")) '("Learn universal sign language")) - (org-ql-expect ((closed :from "2017-07-05")) + (org-ql-expect ('(closed :from "2017-07-05")) '("Learn universal sign language")) (org-ql-then - (org-ql-expect ((closed :from today)) + (org-ql-expect ('(closed :from today)) '("Learn universal sign language"))) - (org-ql-expect ((closed :from "2017-07-06")) + (org-ql-expect ('(closed :from "2017-07-06")) nil)) (org-ql-it ":to" - (org-ql-expect ((closed :to "2017-07-04")) + (org-ql-expect ('(closed :to "2017-07-04")) nil) - (org-ql-expect ((closed :to "2017-07-05")) + (org-ql-expect ('(closed :to "2017-07-05")) '("Learn universal sign language")) (org-ql-then - (org-ql-expect ((closed :to today)) + (org-ql-expect ('(closed :to today)) '("Learn universal sign language"))) - (org-ql-expect ((closed :to "2017-07-06")) + (org-ql-expect ('(closed :to "2017-07-06")) '("Learn universal sign language")))) (describe "(deadline)" (org-ql-it "without arguments" - (org-ql-expect ((deadline)) + (org-ql-expect ('(deadline)) '("Take over the universe" "Take over the world" "Visit Mars" "Visit the moon" "Renew membership in supervillain club" "Internet" "Spaceship lease" "/r/emacs"))) (org-ql-it "auto" (org-ql-then - (org-ql-expect ((deadline auto)) + (org-ql-expect ('(deadline auto)) '("Take over the universe" "Take over the world" "Visit Mars" "Visit the moon" "Renew membership in supervillain club" "Internet" "Spaceship lease" "/r/emacs")))) (org-ql-it "with a number" (org-ql-then - (org-ql-expect ((deadline 2)) + (org-ql-expect ('(deadline 2)) '("Take over the world" "/r/emacs")))) (org-ql-it ":on" - (org-ql-expect ((deadline :on "2017-07-05")) + (org-ql-expect ('(deadline :on "2017-07-05")) '("/r/emacs")) (org-ql-then - (org-ql-expect ((deadline :on today)) + (org-ql-expect ('(deadline :on today)) '("/r/emacs"))) - (org-ql-expect ((deadline :on "2019-06-09")) + (org-ql-expect ('(deadline :on "2019-06-09")) nil)) (org-ql-it ":from" - (org-ql-expect ((deadline :from "2017-07-04")) + (org-ql-expect ('(deadline :from "2017-07-04")) '("Take over the universe" "Take over the world" "Visit Mars" "Visit the moon" "Renew membership in supervillain club" "Internet" "Spaceship lease" "/r/emacs")) - (org-ql-expect ((deadline :from "2017-07-05")) + (org-ql-expect ('(deadline :from "2017-07-05")) '("Take over the universe" "Take over the world" "Visit Mars" "Visit the moon" "Renew membership in supervillain club" "Internet" "Spaceship lease" "/r/emacs")) - (org-ql-expect ((deadline :from "2017-07-06")) + (org-ql-expect ('(deadline :from "2017-07-06")) '("Take over the universe" "Take over the world" "Visit Mars" "Visit the moon" "Renew membership in supervillain club" "Internet" "Spaceship lease")) - (org-ql-expect ((deadline :from "2018-07-06")) + (org-ql-expect ('(deadline :from "2018-07-06")) nil) (org-ql-then - (org-ql-expect ((deadline :from today)) + (org-ql-expect ('(deadline :from today)) '("Take over the universe" "Take over the world" "Visit Mars" "Visit the moon" "Renew membership in supervillain club" "Internet" "Spaceship lease" "/r/emacs")))) (org-ql-it ":to" - (org-ql-expect ((deadline :to "2017-07-04")) + (org-ql-expect ('(deadline :to "2017-07-04")) nil) - (org-ql-expect ((deadline :to "2017-07-05")) + (org-ql-expect ('(deadline :to "2017-07-05")) '("/r/emacs")) - (org-ql-expect ((deadline :to "2018-07-06")) + (org-ql-expect ('(deadline :to "2018-07-06")) '("Take over the universe" "Take over the world" "Visit Mars" "Visit the moon" "Renew membership in supervillain club" "Internet" "Spaceship lease" "/r/emacs")) (org-ql-then - (org-ql-expect ((deadline :to today)) + (org-ql-expect ('(deadline :to today)) '("/r/emacs"))))) (org-ql-it "(done)" - (org-ql-expect ((done)) + (org-ql-expect ('(done)) '("Learn universal sign language"))) (org-ql-it "(habit)" - (org-ql-expect ((habit)) + (org-ql-expect ('(habit)) '("Practice leaping tall buildings in a single bound"))) (describe "(heading)" (org-ql-it "with one argument" - (org-ql-expect ((heading "world")) + (org-ql-expect ('(heading "world")) ;; NOTE: This--correctly--does not match the "Skype with president of ;; Antarctica" heading, which has the tag ":world:" in its heading line. '("Take over the world"))) (org-ql-it "with two arguments" - (org-ql-expect ((heading "Take over" "world")) + (org-ql-expect ('(heading "Take over" "world")) '("Take over the world")))) (describe "(link)" (org-ql-it "without arguments" - (org-ql-expect ((link)) + (org-ql-expect ('(link)) '("/r/emacs"))) (org-ql-it "with description-or-target" - (org-ql-expect ((link "emacs")) + (org-ql-expect ('(link "emacs")) '("/r/emacs"))) (org-ql-it "with :description" - (org-ql-expect ((link :description "emacs")) + (org-ql-expect ('(link :description "emacs")) '("/r/emacs"))) (org-ql-it "with :target" - (org-ql-expect ((link :target "reddit.com")) + (org-ql-expect ('(link :target "reddit.com")) '("/r/emacs"))) (org-ql-it "with :description and :target" - (org-ql-expect ((link :description "emacs" :target "reddit.com")) + (org-ql-expect ('(link :description "emacs" :target "reddit.com")) '("/r/emacs")))) (describe "(outline-path)" (org-ql-it "with one argument" - (org-ql-expect ((outline-path "symphony")) + (org-ql-expect ('(outline-path "symphony")) '("Write a symphony"))) (org-ql-it "with two arguments" - (org-ql-expect ((outline-path "idea" "symphony")) + (org-ql-expect ('(outline-path "idea" "symphony")) '("Write a symphony")))) (describe "(outline-path-segment)" (org-ql-it "with one argument" - (org-ql-expect ((outline-path-segment "symphony")) + (org-ql-expect ('(outline-path-segment "symphony")) '("Write a symphony"))) (org-ql-it "with a contiguous segment" - (org-ql-expect ((outline-path-segment "idea" "symphony")) + (org-ql-expect ('(outline-path-segment "idea" "symphony")) '("Write a symphony"))) (org-ql-it "with a non-contiguous segment" - (org-ql-expect ((outline-path-segment "data" "symphony")) + (org-ql-expect ('(outline-path-segment "data" "symphony")) nil))) (describe "(path)" (org-ql-it "without arguments" - (org-ql-expect ((path)) + (org-ql-expect ('(path)) '("Take over the universe" "Take over the world" "Skype with president of Antarctica" "Take over Mars" "Visit Mars" "Take over the moon" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "Recurring" "/r/emacs" "Shop for groceries" "Sunrise/sunset" "Ideas" "Rewrite Emacs in Common Lisp" "Write a symphony"))) (org-ql-it "with one argument" - (org-ql-expect ((path "data")) + (org-ql-expect ('(path "data")) '("Take over the universe" "Take over the world" "Skype with president of Antarctica" "Take over Mars" "Visit Mars" "Take over the moon" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "Recurring" "/r/emacs" "Shop for groceries" "Sunrise/sunset" "Ideas" "Rewrite Emacs in Common Lisp" "Write a symphony"))) (org-ql-it "with two matching arguments" - (org-ql-expect ((path "data" "tests")) + (org-ql-expect ('(path "data" "tests")) '("Take over the universe" "Take over the world" "Skype with president of Antarctica" "Take over Mars" "Visit Mars" "Take over the moon" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "Recurring" "/r/emacs" "Shop for groceries" "Sunrise/sunset" "Ideas" "Rewrite Emacs in Common Lisp" "Write a symphony"))) (org-ql-it "with two arguments, one matching" - (org-ql-expect ((path "data" "nope")) + (org-ql-expect ('(path "data" "nope")) '("Take over the universe" "Take over the world" "Skype with president of Antarctica" "Take over Mars" "Visit Mars" "Take over the moon" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "Recurring" "/r/emacs" "Shop for groceries" "Sunrise/sunset" "Ideas" "Rewrite Emacs in Common Lisp" "Write a symphony")))) (describe "(planning)" (org-ql-it "without arguments" - (org-ql-expect ((planning)) + (org-ql-expect ('(planning)) '("Take over the universe" "Take over the world" "Skype with president of Antarctica" "Visit Mars" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp"))) (org-ql-it "with a number" (org-ql-then - (org-ql-expect ((planning 2)) + (org-ql-expect ('(planning 2)) '("Take over the world" "Skype with president of Antarctica" "Practice leaping tall buildings in a single bound" "Learn universal sign language" "Order a pizza" "Get haircut" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp")))) (org-ql-it ":on" - (org-ql-expect ((planning :on "2017-07-05")) + (org-ql-expect ('(planning :on "2017-07-05")) '("Practice leaping tall buildings in a single bound" "Learn universal sign language" "Order a pizza" "Get haircut" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp")) - (org-ql-expect ((planning :on "2019-06-09")) + (org-ql-expect ('(planning :on "2019-06-09")) nil) (org-ql-then - (org-ql-expect ((planning :on today)) + (org-ql-expect ('(planning :on today)) '("Practice leaping tall buildings in a single bound" "Learn universal sign language" "Order a pizza" "Get haircut" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp")))) (org-ql-it ":from" - (org-ql-expect ((planning :from "2017-07-04")) + (org-ql-expect ('(planning :from "2017-07-04")) '("Take over the universe" "Take over the world" "Skype with president of Antarctica" "Visit Mars" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp")) - (org-ql-expect ((planning :from "2017-07-05")) + (org-ql-expect ('(planning :from "2017-07-05")) '("Take over the universe" "Take over the world" "Visit Mars" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp")) - (org-ql-expect ((planning :from "2017-07-06")) + (org-ql-expect ('(planning :from "2017-07-06")) '("Take over the universe" "Take over the world" "Visit Mars" "Visit the moon" "Renew membership in supervillain club" "Internet" "Spaceship lease")) (org-ql-then - (org-ql-expect ((planning :from today)) + (org-ql-expect ('(planning :from today)) '("Take over the universe" "Take over the world" "Visit Mars" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp")))) (org-ql-it ":to" - (org-ql-expect ((planning :to "2017-07-04")) + (org-ql-expect ('(planning :to "2017-07-04")) '("Skype with president of Antarctica")) - (org-ql-expect ((planning :to "2017-07-05")) + (org-ql-expect ('(planning :to "2017-07-05")) '("Skype with president of Antarctica" "Practice leaping tall buildings in a single bound" "Learn universal sign language" "Order a pizza" "Get haircut" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp")) - (org-ql-expect ((planning :to "2018-07-06")) + (org-ql-expect ('(planning :to "2018-07-06")) '("Take over the universe" "Take over the world" "Skype with president of Antarctica" "Visit Mars" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp")) (org-ql-then - (org-ql-expect ((planning :to today)) + (org-ql-expect ('(planning :to today)) '("Skype with president of Antarctica" "Practice leaping tall buildings in a single bound" "Learn universal sign language" "Order a pizza" "Get haircut" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp"))))) (describe "(priority)" (org-ql-it "without arguments" - (org-ql-expect ((priority)) + (org-ql-expect ('(priority)) '("Take over the universe" "Take over the world" "Skype with president of Antarctica" "Take over Mars" "Take over the moon" "Renew membership in supervillain club" "Learn universal sign language" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor"))) (org-ql-it "with a priority" - (org-ql-expect ((priority "A")) + (org-ql-expect ('(priority "A")) '("Take over the universe" "Take over the world" "Skype with president of Antarctica" "Spaceship lease"))) (org-ql-it "= a priority" - (org-ql-expect ((priority = "A")) + (org-ql-expect ('(priority = "A")) '("Take over the universe" "Take over the world" "Skype with president of Antarctica" "Spaceship lease"))) (org-ql-it "< a priority" - (org-ql-expect ((priority < "B")) + (org-ql-expect ('(priority < "B")) '("Take over the moon" "Get haircut"))) (org-ql-it "<= a priority" - (org-ql-expect ((priority <= "B")) + (org-ql-expect ('(priority <= "B")) '("Take over Mars" "Take over the moon" "Renew membership in supervillain club" "Learn universal sign language" "Get haircut" "Internet" "Fix flux capacitor"))) (org-ql-it "> a priority" - (org-ql-expect ((priority > "B")) + (org-ql-expect ('(priority > "B")) '("Take over the universe" "Take over the world" "Skype with president of Antarctica" "Spaceship lease"))) (org-ql-it ">= a priority" - (org-ql-expect ((priority >= "B")) + (org-ql-expect ('(priority >= "B")) '("Take over the universe" "Take over the world" "Skype with president of Antarctica" "Take over Mars" "Renew membership in supervillain club" "Learn universal sign language" "Internet" "Spaceship lease" "Fix flux capacitor")))) (describe "(property)" ;; MAYBE: Add support for (property) without arguments. ;; (org-ql-it "without arguments" - ;; (org-ql-expect ((property)))) + ;; (org-ql-expect ('(property)))) (org-ql-it "with a property" - (org-ql-expect ((property "agenda-group")) + (org-ql-expect ('(property "agenda-group")) '("Take over the universe" "Spaceship lease" "Recurring" "Write a symphony"))) (org-ql-it "with a property and a value" - (org-ql-expect ((property "agenda-group" "plans")) + (org-ql-expect ('(property "agenda-group" "plans")) '("Take over the universe" "Write a symphony")))) (describe "(regexp)" (org-ql-it "with 1 argument" - (org-ql-expect ((regexp "Take over") - :sort todo) + (org-ql-expect ('(regexp "Take over") + :sort 'todo) '("Take over the universe" "Take over the world" "Take over Mars" "Take over the moon" "Get haircut"))) (org-ql-it "with 2 arguments" - (org-ql-expect ((regexp "Take over" "universe") - :sort todo) + (org-ql-expect ('(regexp "Take over" "universe") + :sort 'todo) '("Take over the universe"))) (org-ql-it "with a plain string" (org-ql-expect ("Take over" - :sort todo) + :sort 'todo) '("Take over the universe" "Take over the world" "Take over Mars" "Take over the moon" "Get haircut"))) (org-ql-it "with two plain strings in an OR" - (org-ql-expect ((or "Take over" "universe") - :sort todo) + (org-ql-expect ('(or "Take over" "universe") + :sort 'todo) '("Take over the universe" "Take over the world" "Take over Mars" "Take over the moon" "Get haircut"))) (org-ql-it "case-folding predicate with non-case-folding preamble" ;; e.g. the (todo) predicate disables case-folding in its preamble, but that ;; should not prevent case-folding in this and other predicates (issue #114). - (org-ql-expect ((and (todo "TODO") (regexp "take over")) - :sort todo) + (org-ql-expect ('(and (todo "TODO") (regexp "take over")) + :sort 'todo) '("Take over the universe" "Take over the world" "Take over Mars" "Take over the moon" "Get haircut")))) (describe "(scheduled)" (org-ql-it "without arguments" - (org-ql-expect ((scheduled)) + (org-ql-expect ('(scheduled)) '("Skype with president of Antarctica" "Practice leaping tall buildings in a single bound" "Order a pizza" "Get haircut" "Fix flux capacitor" "Shop for groceries" "Rewrite Emacs in Common Lisp"))) (org-ql-it "with a number" (org-ql-then ;; Using -1 is the easiest way to exclude some results but not all for testing this. - (org-ql-expect ((scheduled -1)) + (org-ql-expect ('(scheduled -1)) '("Skype with president of Antarctica")))) (org-ql-it ":on" - (org-ql-expect ((scheduled :on "2017-07-05")) + (org-ql-expect ('(scheduled :on "2017-07-05")) '("Practice leaping tall buildings in a single bound" "Order a pizza" "Get haircut" "Fix flux capacitor" "Shop for groceries" "Rewrite Emacs in Common Lisp")) - (org-ql-expect ((scheduled :on "2019-06-09")) + (org-ql-expect ('(scheduled :on "2019-06-09")) nil) (org-ql-then - (org-ql-expect ((scheduled :on today)) + (org-ql-expect ('(scheduled :on today)) '("Practice leaping tall buildings in a single bound" "Order a pizza" "Get haircut" "Fix flux capacitor" "Shop for groceries" "Rewrite Emacs in Common Lisp")))) (org-ql-it ":from" - (org-ql-expect ((scheduled :from "2017-07-04")) + (org-ql-expect ('(scheduled :from "2017-07-04")) '("Skype with president of Antarctica" "Practice leaping tall buildings in a single bound" "Order a pizza" "Get haircut" "Fix flux capacitor" "Shop for groceries" "Rewrite Emacs in Common Lisp")) - (org-ql-expect ((scheduled :from "2017-07-05")) + (org-ql-expect ('(scheduled :from "2017-07-05")) '("Practice leaping tall buildings in a single bound" "Order a pizza" "Get haircut" "Fix flux capacitor" "Shop for groceries" "Rewrite Emacs in Common Lisp")) - (org-ql-expect ((scheduled :from "2017-07-06")) + (org-ql-expect ('(scheduled :from "2017-07-06")) nil) (org-ql-then - (org-ql-expect ((scheduled :from today)) + (org-ql-expect ('(scheduled :from today)) '("Practice leaping tall buildings in a single bound" "Order a pizza" "Get haircut" "Fix flux capacitor" "Shop for groceries" "Rewrite Emacs in Common Lisp")))) (org-ql-it ":to" - (org-ql-expect ((scheduled :to "2017-07-04")) + (org-ql-expect ('(scheduled :to "2017-07-04")) '("Skype with president of Antarctica")) - (org-ql-expect ((scheduled :to "2017-07-05")) + (org-ql-expect ('(scheduled :to "2017-07-05")) '("Skype with president of Antarctica" "Practice leaping tall buildings in a single bound" "Order a pizza" "Get haircut" "Fix flux capacitor" "Shop for groceries" "Rewrite Emacs in Common Lisp")) - (org-ql-expect ((scheduled :to "2018-07-06")) + (org-ql-expect ('(scheduled :to "2018-07-06")) '("Skype with president of Antarctica" "Practice leaping tall buildings in a single bound" "Order a pizza" "Get haircut" "Fix flux capacitor" "Shop for groceries" "Rewrite Emacs in Common Lisp")) (org-ql-then - (org-ql-expect ((scheduled :to today)) + (org-ql-expect ('(scheduled :to today)) '("Skype with president of Antarctica" "Practice leaping tall buildings in a single bound" "Order a pizza" "Get haircut" "Fix flux capacitor" "Shop for groceries" "Rewrite Emacs in Common Lisp"))))) ;; TODO: Test (src) predicate. That will require modifying test data, which will be a @@ -847,147 +848,147 @@ RESULTS should be a list of strings as returned by (describe "(todo)" (org-ql-it "without arguments" - (org-ql-expect ((todo) - :sort todo) + (org-ql-expect ('(todo) + :sort 'todo) '("Take over the universe" "Take over the world" "Skype with president of Antarctica" "Take over Mars" "Visit Mars" "Take over the moon" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp" "Write a symphony"))) (org-ql-it "with 1 argument" - (org-ql-expect ((todo "WAITING") - :sort todo) + (org-ql-expect ('(todo "WAITING") + :sort 'todo) '("Visit the moon"))) (org-ql-it "with 2 arguments" - (org-ql-expect ((todo "WAITING" "SOMEDAY") - :sort todo) + (org-ql-expect ('(todo "WAITING" "SOMEDAY") + :sort 'todo) '("Visit the moon" "Rewrite Emacs in Common Lisp" "Write a symphony")))) (describe "(tags)" (org-ql-it "without arguments" - (org-ql-expect ((tags)) + (org-ql-expect ('(tags)) '("Take over the universe" "Take over the world" "Skype with president of Antarctica" "Take over Mars" "Visit Mars" "Take over the moon" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp" "Write a symphony")) - (org-ql-expect ((not (tags))) + (org-ql-expect ('(not (tags))) '("Recurring" "Sunrise/sunset" "Ideas"))) (org-ql-it "with a tag" - (org-ql-expect ((tags "Emacs")) + (org-ql-expect ('(tags "Emacs")) '("/r/emacs" "Rewrite Emacs in Common Lisp")) - (org-ql-expect ((not (tags "Emacs"))) + (org-ql-expect ('(not (tags "Emacs"))) '("Take over the universe" "Take over the world" "Skype with president of Antarctica" "Take over Mars" "Visit Mars" "Take over the moon" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "Recurring" "Shop for groceries" "Sunrise/sunset" "Ideas" "Write a symphony"))) (org-ql-it "with 2 tags" - (org-ql-expect ((tags "Emacs" "space")) + (org-ql-expect ('(tags "Emacs" "space")) '("Visit Mars" "Visit the moon" "/r/emacs" "Rewrite Emacs in Common Lisp")) - (org-ql-expect ((not (tags "Emacs" "space"))) + (org-ql-expect ('(not (tags "Emacs" "space"))) '("Take over the universe" "Take over the world" "Skype with president of Antarctica" "Take over Mars" "Take over the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "Recurring" "Shop for groceries" "Sunrise/sunset" "Ideas" "Write a symphony"))) (org-ql-it "with file tags" - (org-ql-expect ((tags "food")) + (org-ql-expect ('(tags "food")) '("Fruit" "Blueberry" "Strawberry" "Vegetable" "Broccoli" "Potato") :buffer (org-ql-test-data-buffer "data2.org")) - (org-ql-expect ((tags "fruit")) + (org-ql-expect ('(tags "fruit")) '("Fruit" "Blueberry" "Strawberry") :buffer (org-ql-test-data-buffer "data2.org")))) (describe "(tags-inherited)" (org-ql-it "without arguments" - (org-ql-expect ((tags-inherited)) + (org-ql-expect ('(tags-inherited)) '("Take over the world" "Skype with president of Antarctica" "Take over Mars" "Visit Mars" "Take over the moon" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language")) - (org-ql-expect ((not (inherited-tags))) + (org-ql-expect ('(not (inherited-tags))) '("Take over the universe" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "Recurring" "/r/emacs" "Shop for groceries" "Sunrise/sunset" "Ideas" "Rewrite Emacs in Common Lisp" "Write a symphony"))) (org-ql-it "with a tag" - (org-ql-expect ((tags-inherited "Emacs")) + (org-ql-expect ('(tags-inherited "Emacs")) nil) - (org-ql-expect ((itags "ambition")) + (org-ql-expect ('(itags "ambition")) '("Take over the world" "Skype with president of Antarctica" "Take over Mars" "Visit Mars" "Take over the moon" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language")) - (org-ql-expect ((not (tags-i "ambition"))) + (org-ql-expect ('(not (tags-i "ambition"))) '("Take over the universe" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "Recurring" "/r/emacs" "Shop for groceries" "Sunrise/sunset" "Ideas" "Rewrite Emacs in Common Lisp" "Write a symphony"))) (org-ql-it "with 2 tags" - (org-ql-expect ((itags "personal" "world")) + (org-ql-expect ('(itags "personal" "world")) '("Skype with president of Antarctica")) - (org-ql-expect ((not (tags-inherited "personal" "world"))) + (org-ql-expect ('(not (tags-inherited "personal" "world"))) ;; Note that this correctly includes the task "Practice leaping...", which has the LOCAL tag "personal". '("Take over the universe" "Take over the world" "Take over Mars" "Visit Mars" "Take over the moon" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "Recurring" "/r/emacs" "Shop for groceries" "Sunrise/sunset" "Ideas" "Rewrite Emacs in Common Lisp" "Write a symphony"))) (org-ql-it "with file tags" - (org-ql-expect ((tags-inherited "food")) + (org-ql-expect ('(tags-inherited "food")) '("Fruit" "Blueberry" "Strawberry" "Vegetable" "Broccoli" "Potato") :buffer (org-ql-test-data-buffer "data2.org")) - (org-ql-expect ((tags-inherited "fruit")) + (org-ql-expect ('(tags-inherited "fruit")) '("Blueberry" "Strawberry") :buffer (org-ql-test-data-buffer "data2.org")))) (describe "(tags-local)" (org-ql-it "without arguments" - (org-ql-expect ((tags-local)) + (org-ql-expect ('(tags-local)) '("Take over the universe" "Take over the world" "Skype with president of Antarctica" "Visit Mars" "Visit the moon" "Practice leaping tall buildings in a single bound" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp" "Write a symphony")) - (org-ql-expect ((not (local-tags))) + (org-ql-expect ('(not (local-tags))) '("Take over Mars" "Take over the moon" "Renew membership in supervillain club" "Learn universal sign language" "Recurring" "Sunrise/sunset" "Ideas"))) (org-ql-it "with a tag" - (org-ql-expect ((tags-local "world")) + (org-ql-expect ('(tags-local "world")) '("Take over the world" "Skype with president of Antarctica")) - (org-ql-expect ((ltags "ambition")) + (org-ql-expect ('(ltags "ambition")) '("Take over the universe")) - (org-ql-expect ((not (tags-l "ambition"))) + (org-ql-expect ('(not (tags-l "ambition"))) '("Take over the world" "Skype with president of Antarctica" "Take over Mars" "Visit Mars" "Take over the moon" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "Recurring" "/r/emacs" "Shop for groceries" "Sunrise/sunset" "Ideas" "Rewrite Emacs in Common Lisp" "Write a symphony"))) (org-ql-it "with 2 tags" - (org-ql-expect ((ltags "personal" "world")) + (org-ql-expect ('(ltags "personal" "world")) '("Take over the world" "Skype with president of Antarctica" "Practice leaping tall buildings in a single bound" "Get haircut")) - (org-ql-expect ((not (tags-local "personal" "world"))) + (org-ql-expect ('(not (tags-local "personal" "world"))) '("Take over the universe" "Take over Mars" "Visit Mars" "Take over the moon" "Visit the moon" "Renew membership in supervillain club" "Learn universal sign language" "Order a pizza" "Internet" "Spaceship lease" "Fix flux capacitor" "Recurring" "/r/emacs" "Shop for groceries" "Sunrise/sunset" "Ideas" "Rewrite Emacs in Common Lisp" "Write a symphony"))) (org-ql-it "with file tags" - (org-ql-expect ((tags-local "food")) + (org-ql-expect ('(tags-local "food")) nil :buffer (org-ql-test-data-buffer "data2.org")) - (org-ql-expect ((tags-local "fruit")) + (org-ql-expect ('(tags-local "fruit")) '("Fruit") :buffer (org-ql-test-data-buffer "data2.org")))) (describe "(tags-all), (tags&)" (org-ql-it "with 2 tags" - (org-ql-expect ((tags-all "universe" "personal")) + (org-ql-expect ('(tags-all "universe" "personal")) '("Practice leaping tall buildings in a single bound")) - (org-ql-expect ((tags& "ambition" "space")) + (org-ql-expect ('(tags& "ambition" "space")) '("Visit Mars" "Visit the moon"))) (org-ql-it "with file tags" - (org-ql-expect ((tags-all "food" "fruit")) + (org-ql-expect ('(tags-all "food" "fruit")) '("Fruit" "Blueberry" "Strawberry") :buffer (org-ql-test-data-buffer "data2.org")))) (describe "(tags-regexp), (tags*)" (org-ql-it "without arguments" - (org-ql-expect ((tags-regexp)) + (org-ql-expect ('(tags-regexp)) '("Take over the universe" "Take over the world" "Skype with president of Antarctica" "Take over Mars" "Visit Mars" "Take over the moon" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp" "Write a symphony")) - (org-ql-expect ((not (tags*))) + (org-ql-expect ('(not (tags*))) '("Recurring" "Sunrise/sunset" "Ideas"))) (org-ql-it "with a tag regexp" - (org-ql-expect ((tags-regexp "Emac")) + (org-ql-expect ('(tags-regexp "Emac")) '("/r/emacs" "Rewrite Emacs in Common Lisp")) - (org-ql-expect ((not (tags* "Emac"))) + (org-ql-expect ('(not (tags* "Emac"))) '("Take over the universe" "Take over the world" "Skype with president of Antarctica" "Take over Mars" "Visit Mars" "Take over the moon" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "Recurring" "Shop for groceries" "Sunrise/sunset" "Ideas" "Write a symphony"))) (org-ql-it "with 2 tag regexps" - (org-ql-expect ((tags-regexp "Emac" "spac")) + (org-ql-expect ('(tags-regexp "Emac" "spac")) '("Visit Mars" "Visit the moon" "Spaceship lease" "Fix flux capacitor" "/r/emacs" "Rewrite Emacs in Common Lisp")) - (org-ql-expect ((not (tags* "Emac" "spac"))) + (org-ql-expect ('(not (tags* "Emac" "spac"))) '("Take over the universe" "Take over the world" "Skype with president of Antarctica" "Take over Mars" "Take over the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "Order a pizza" "Get haircut" "Internet" "Recurring" "Shop for groceries" "Sunrise/sunset" "Ideas" "Write a symphony"))) (org-ql-it "with regexp matching file tags" - (org-ql-expect ((tags-regexp "foo")) + (org-ql-expect ('(tags-regexp "foo")) '("Fruit" "Blueberry" "Strawberry" "Vegetable" "Broccoli" "Potato") :buffer (org-ql-test-data-buffer "data2.org")) - (org-ql-expect ((tags* "frui")) + (org-ql-expect ('(tags* "frui")) '("Fruit" "Blueberry" "Strawberry") :buffer (org-ql-test-data-buffer "data2.org")))) @@ -996,166 +997,166 @@ RESULTS should be a list of strings as returned by (describe "active" (org-ql-it "without arguments" - (org-ql-expect ((ts :type active)) + (org-ql-expect ('(ts :type active)) '("Take over the universe" "Take over the world" "Skype with president of Antarctica" "Visit Mars" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp"))) (org-ql-it ":from a timestamp" - (org-ql-expect ((ts :from "2017-07-08" :type active)) + (org-ql-expect ('(ts :from "2017-07-08" :type active)) '("Take over the universe" "Visit Mars" "Visit the moon" "Renew membership in supervillain club" "Internet" "Spaceship lease")) - (org-ql-expect ((ts :from "2019-06-08" :type active)) + (org-ql-expect ('(ts :from "2019-06-08" :type active)) nil) (org-ql-then - (org-ql-expect ((ts :from today)) + (org-ql-expect ('(ts :from today)) '("Take over the universe" "Take over the world" "Visit Mars" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp")))) (org-ql-it ":from a number of days" (org-ql-then - (org-ql-expect ((ts :from 5)) + (org-ql-expect ('(ts :from 5)) '("Take over the universe" "Visit Mars" "Visit the moon" "Renew membership in supervillain club" "Internet" "Spaceship lease" "Rewrite Emacs in Common Lisp")))) (org-ql-it ":to a timestamp" - (org-ql-expect ((ts :to "2019-06-10" :type active)) + (org-ql-expect ('(ts :to "2019-06-10" :type active)) '("Take over the universe" "Take over the world" "Skype with president of Antarctica" "Visit Mars" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp")) - (org-ql-expect ((ts :to "2017-07-04" :type active)) + (org-ql-expect ('(ts :to "2017-07-04" :type active)) '("Skype with president of Antarctica")) (org-ql-then - (org-ql-expect ((ts :to today)) + (org-ql-expect ('(ts :to today)) '("Skype with president of Antarctica" "Practice leaping tall buildings in a single bound" "Learn universal sign language" "Order a pizza" "Get haircut" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp")))) (org-ql-it ":to a number of days" (org-ql-then - (org-ql-expect ((ts :to -1)) + (org-ql-expect ('(ts :to -1)) '("Skype with president of Antarctica")))) (org-ql-it ":on a timestamp" - (org-ql-expect ((ts :on "2017-07-05" :type active)) + (org-ql-expect ('(ts :on "2017-07-05" :type active)) '("Practice leaping tall buildings in a single bound" "Order a pizza" "Get haircut" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp")) - (org-ql-expect ((ts :on "2019-06-09" :type active)) + (org-ql-expect ('(ts :on "2019-06-09" :type active)) nil) (org-ql-then - (org-ql-expect ((ts :on today)) + (org-ql-expect ('(ts :on today)) '("Practice leaping tall buildings in a single bound" "Learn universal sign language" "Order a pizza" "Get haircut" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp")))) (org-ql-it ":on a number of days" (org-ql-then - (org-ql-expect ((ts-active :on 2)) + (org-ql-expect ('(ts-active :on 2)) '("Take over the world"))))) (describe "inactive" (org-ql-it "without arguments" - (org-ql-expect ((ts :type inactive)) + (org-ql-expect ('(ts :type inactive)) '("Visit the moon" "Learn universal sign language" "Rewrite Emacs in Common Lisp"))) (org-ql-it ":from a timestamp" - (org-ql-expect ((ts :from "2017-07-06" :type inactive)) + (org-ql-expect ('(ts :from "2017-07-06" :type inactive)) '("Visit the moon" "Rewrite Emacs in Common Lisp")) - (org-ql-expect ((ts :from "2019-06-08" :type inactive)) + (org-ql-expect ('(ts :from "2019-06-08" :type inactive)) nil) (org-ql-then - (org-ql-expect ((ts-inactive :from today)) + (org-ql-expect ('(ts-inactive :from today)) '("Visit the moon" "Learn universal sign language" "Rewrite Emacs in Common Lisp")))) (org-ql-it ":from a number of days" (org-ql-then - (org-ql-expect ((ts-i :from 5)) + (org-ql-expect ('(ts-i :from 5)) '("Visit the moon" "Rewrite Emacs in Common Lisp")))) (org-ql-it ":to a timestamp" - (org-ql-expect ((ts :to "2019-06-10" :type inactive)) + (org-ql-expect ('(ts :to "2019-06-10" :type inactive)) '("Visit the moon" "Learn universal sign language" "Rewrite Emacs in Common Lisp")) - (org-ql-expect ((ts :to "2017-07-04" :type inactive)) + (org-ql-expect ('(ts :to "2017-07-04" :type inactive)) 'nil) (org-ql-then - (org-ql-expect ((ts-inactive :to today)) + (org-ql-expect ('(ts-inactive :to today)) '("Learn universal sign language")))) (org-ql-it ":to a number of days" (org-ql-then - (org-ql-expect ((ts-i :to 5)) + (org-ql-expect ('(ts-i :to 5)) '("Learn universal sign language")))) (org-ql-it ":on a timestamp" - (org-ql-expect ((ts :on "2017-07-05" :type inactive)) + (org-ql-expect ('(ts :on "2017-07-05" :type inactive)) '("Learn universal sign language")) - (org-ql-expect ((ts :on "2019-06-09" :type inactive)) + (org-ql-expect ('(ts :on "2019-06-09" :type inactive)) nil) (org-ql-then - (org-ql-expect ((ts-inactive :on today)) + (org-ql-expect ('(ts-inactive :on today)) '("Learn universal sign language")))) (org-ql-it ":on a number of days" (org-ql-then - (org-ql-expect ((ts-inactive :on 19)) + (org-ql-expect ('(ts-inactive :on 19)) '("Visit the moon" "Rewrite Emacs in Common Lisp"))))) (describe "both" (org-ql-it "without arguments" - (org-ql-expect ((ts)) + (org-ql-expect ('(ts)) '("Take over the universe" "Take over the world" "Skype with president of Antarctica" "Visit Mars" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp")) - (org-ql-expect ((ts :type both)) + (org-ql-expect ('(ts :type both)) '("Take over the universe" "Take over the world" "Skype with president of Antarctica" "Visit Mars" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp"))) (org-ql-it ":from a timestamp" - (org-ql-expect ((ts :from "2017-07-05")) + (org-ql-expect ('(ts :from "2017-07-05")) '("Take over the universe" "Take over the world" "Visit Mars" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp")) - (org-ql-expect ((ts :from "2017-07-05" :type both)) + (org-ql-expect ('(ts :from "2017-07-05" :type both)) '("Take over the universe" "Take over the world" "Visit Mars" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp")) - (org-ql-expect ((ts :from "2019-06-08")) + (org-ql-expect ('(ts :from "2019-06-08")) nil) - (org-ql-expect ((ts :from "2019-06-08" :type both)) + (org-ql-expect ('(ts :from "2019-06-08" :type both)) nil) (org-ql-then - (org-ql-expect ((ts :from today)) + (org-ql-expect ('(ts :from today)) '("Take over the universe" "Take over the world" "Visit Mars" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp")))) (org-ql-it ":from a number of days" (org-ql-then - (org-ql-expect ((ts :from -5)) + (org-ql-expect ('(ts :from -5)) '("Take over the universe" "Take over the world" "Skype with president of Antarctica" "Visit Mars" "Visit the moon" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "Order a pizza" "Get haircut" "Internet" "Spaceship lease" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp")))) (org-ql-it ":to a timestamp" - (org-ql-expect ((ts :to "2017-07-06")) + (org-ql-expect ('(ts :to "2017-07-06")) '("Skype with president of Antarctica" "Practice leaping tall buildings in a single bound" "Learn universal sign language" "Order a pizza" "Get haircut" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp")) - (org-ql-expect ((ts :to "2017-07-06" :type both)) + (org-ql-expect ('(ts :to "2017-07-06" :type both)) '("Skype with president of Antarctica" "Practice leaping tall buildings in a single bound" "Learn universal sign language" "Order a pizza" "Get haircut" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp")) - (org-ql-expect ((ts :to "2017-07-04")) + (org-ql-expect ('(ts :to "2017-07-04")) '("Skype with president of Antarctica")) - (org-ql-expect ((ts :to "2017-07-04" :type both)) + (org-ql-expect ('(ts :to "2017-07-04" :type both)) '("Skype with president of Antarctica")) (org-ql-then - (org-ql-expect ((ts :to today)) + (org-ql-expect ('(ts :to today)) '("Skype with president of Antarctica" "Practice leaping tall buildings in a single bound" "Learn universal sign language" "Order a pizza" "Get haircut" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp")))) (org-ql-it ":to a number of days" (org-ql-then - (org-ql-expect ((ts :to 5)) + (org-ql-expect ('(ts :to 5)) '("Take over the world" "Skype with president of Antarctica" "Practice leaping tall buildings in a single bound" "Renew membership in supervillain club" "Learn universal sign language" "Order a pizza" "Get haircut" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp")))) (org-ql-it ":on a timestamp" - (org-ql-expect ((ts :on "2017-07-05")) + (org-ql-expect ('(ts :on "2017-07-05")) '("Practice leaping tall buildings in a single bound" "Learn universal sign language" "Order a pizza" "Get haircut" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp")) - (org-ql-expect ((ts :on "2017-07-05" :type both)) + (org-ql-expect ('(ts :on "2017-07-05" :type both)) '("Practice leaping tall buildings in a single bound" "Learn universal sign language" "Order a pizza" "Get haircut" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp")) - (org-ql-expect ((ts :on "2019-06-09")) + (org-ql-expect ('(ts :on "2019-06-09")) nil) - (org-ql-expect ((ts :on "2019-06-09" :type both)) + (org-ql-expect ('(ts :on "2019-06-09" :type both)) nil) (org-ql-then - (org-ql-expect ((ts :on today)) + (org-ql-expect ('(ts :on today)) '("Practice leaping tall buildings in a single bound" "Learn universal sign language" "Order a pizza" "Get haircut" "Fix flux capacitor" "/r/emacs" "Shop for groceries" "Rewrite Emacs in Common Lisp")))) (org-ql-it ":on a number of days" (org-ql-then - (org-ql-expect ((ts :on 5)) + (org-ql-expect ('(ts :on 5)) '("Renew membership in supervillain club")))))) (describe "Compound queries" (org-ql-it "Tags and to-do" - (org-ql-expect ((and (todo "SOMEDAY") - (tags "Emacs"))) + (org-ql-expect ('(and (todo "SOMEDAY") + (tags "Emacs"))) '("Rewrite Emacs in Common Lisp"))))) (describe "Org link safety" @@ -1469,7 +1470,7 @@ RESULTS should be a list of strings as returned by :super-groups super-groups :sort sort :title title :buffer view-buffer) (with-current-buffer view-buffer - (cl-assert (member '("org-ql-search" :follow org-ql-view--link-open :store org-ql-view--link-store) + (cl-assert (member '("org-ql-search" :follow org-ql-view--link-follow :store org-ql-view--link-store) org-link-parameters) t) (with-simulated-input store-input From 3924b023e0f1e8fa93cfa79bdd71f9f0c0fb4679 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Sun, 22 Nov 2020 18:19:02 -0600 Subject: [PATCH 071/372] Release: 0.5 --- README.org | 2 +- org-ql.el | 2 +- org-ql.info | 106 ++++++++++++++++++++++++++-------------------------- 3 files changed, 55 insertions(+), 55 deletions(-) diff --git a/README.org b/README.org index fed38c4..b3e7e42 100644 --- a/README.org +++ b/README.org @@ -461,7 +461,7 @@ Simple links may also be written manually in either sexp or non-sexp form, like: /Note:/ Breaking changes may be made before version 1.0, but in the event of major changes, attempts at backward compatibility will be made with obsolescence declarations, translation of arguments, etc. Users who need stability guarantees before 1.0 may choose to use tagged stable releases. -** 0.5-pre +** 0.5 *Added* + View dispatcher using =transient.el= (like Magit), bound to =v= in search/view buffers. diff --git a/org-ql.el b/org-ql.el index 3e38088..70fefe0 100644 --- a/org-ql.el +++ b/org-ql.el @@ -2,7 +2,7 @@ ;; Author: Adam Porter ;; Url: https://github.com/alphapapa/org-ql -;; Version: 0.5-pre +;; Version: 0.5 ;; Package-Requires: ((emacs "26.1") (dash "2.13") (dash-functional "1.2.0") (f "0.17.2") (map "2.1") (org "9.0") (org-super-agenda "1.2") (ov "1.0.6") (peg "0.6") (s "1.12.0") (transient "0.1") (ts "0.2-pre")) ;; Keywords: hypermedia, outlines, Org, agenda diff --git a/org-ql.info b/org-ql.info index 3e8c933..60d0048 100644 --- a/org-ql.info +++ b/org-ql.info @@ -66,7 +66,7 @@ Functions / Macros Changelog -* 0.5-pre: 05-pre. +* 0.5: 05. * 0.4.9: 049. * 0.4.8: 048. * 0.4.7: 047. @@ -831,7 +831,7 @@ releases. * Menu: -* 0.5-pre: 05-pre. +* 0.5: 05. * 0.4.9: 049. * 0.4.8: 048. * 0.4.7: 047. @@ -852,10 +852,10 @@ releases. * 0.1: 01.  -File: README.info, Node: 05-pre, Next: 049, Up: Changelog +File: README.info, Node: 05, Next: 049, Up: Changelog -5.1 0.5-pre -=========== +5.1 0.5 +======= *Added* • View dispatcher using transient.el (like Magit), bound to v in @@ -893,7 +893,7 @@ File: README.info, Node: 05-pre, Next: 049, Up: Changelog testing new features in this version.  -File: README.info, Node: 049, Next: 048, Prev: 05-pre, Up: Changelog +File: README.info, Node: 049, Next: 048, Prev: 05, Up: Changelog 5.2 0.4.9 ========= @@ -1338,53 +1338,53 @@ GPLv3  Tag Table: Node: Top225 -Node: Contents1594 -Node: Screenshots1717 -Node: Installation1835 -Node: Quelpa2473 -Node: Usage2916 -Node: Commands3314 -Node: org-ql-search3787 -Node: helm-org-ql5437 -Node: org-ql-view5849 -Node: org-ql-view-sidebar6349 -Node: org-ql-view-recent-items6705 -Node: org-ql-sparse-tree7189 -Node: Queries7989 -Node: Non-sexp query syntax9100 -Node: General predicates10807 -Node: Ancestor/descendant predicates16220 -Node: Date/time predicates17348 -Node: Functions / Macros20003 -Node: Agenda-like views20258 -Node: Listing / acting-on results21663 -Node: Dynamic block26858 -Node: Links29556 -Node: Tips30243 -Node: Changelog30561 -Node: 05-pre31245 -Node: 04932719 -Node: 04832997 -Node: 04733344 -Node: 04633739 -Node: 04534139 -Node: 04434498 -Node: 04334855 -Node: 04235050 -Node: 04135209 -Node: 0435450 -Node: 03239383 -Node: 03139762 -Node: 0339959 -Node: 02342934 -Node: 02243162 -Node: 02143430 -Node: 0243629 -Node: 0147664 -Node: Notes47765 -Node: Comparison with Org Agenda searches47927 -Node: org-sidebar48799 -Node: License49078 +Node: Contents1586 +Node: Screenshots1709 +Node: Installation1827 +Node: Quelpa2465 +Node: Usage2908 +Node: Commands3306 +Node: org-ql-search3779 +Node: helm-org-ql5429 +Node: org-ql-view5841 +Node: org-ql-view-sidebar6341 +Node: org-ql-view-recent-items6697 +Node: org-ql-sparse-tree7181 +Node: Queries7981 +Node: Non-sexp query syntax9092 +Node: General predicates10799 +Node: Ancestor/descendant predicates16212 +Node: Date/time predicates17340 +Node: Functions / Macros19995 +Node: Agenda-like views20250 +Node: Listing / acting-on results21655 +Node: Dynamic block26850 +Node: Links29548 +Node: Tips30235 +Node: Changelog30553 +Node: 0531229 +Node: 04932691 +Node: 04832965 +Node: 04733312 +Node: 04633707 +Node: 04534107 +Node: 04434466 +Node: 04334823 +Node: 04235018 +Node: 04135177 +Node: 0435418 +Node: 03239351 +Node: 03139730 +Node: 0339927 +Node: 02342902 +Node: 02243130 +Node: 02143398 +Node: 0243597 +Node: 0147632 +Node: Notes47733 +Node: Comparison with Org Agenda searches47895 +Node: org-sidebar48767 +Node: License49046  End Tag Table From 148a193ca09289fb4c394f30297a6698856bf56e Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Sun, 22 Nov 2020 19:06:16 -0600 Subject: [PATCH 072/372] Meta: 0.6-pre --- README.org | 4 ++ org-ql.el | 2 +- org-ql.info | 147 ++++++++++++++++++++++++++++------------------------ 3 files changed, 84 insertions(+), 69 deletions(-) diff --git a/README.org b/README.org index b3e7e42..b4f8596 100644 --- a/README.org +++ b/README.org @@ -461,6 +461,10 @@ Simple links may also be written manually in either sexp or non-sexp form, like: /Note:/ Breaking changes may be made before version 1.0, but in the event of major changes, attempts at backward compatibility will be made with obsolescence declarations, translation of arguments, etc. Users who need stability guarantees before 1.0 may choose to use tagged stable releases. +** 0.6-pre + +Nothing new yet. + ** 0.5 *Added* diff --git a/org-ql.el b/org-ql.el index 70fefe0..bd53108 100644 --- a/org-ql.el +++ b/org-ql.el @@ -2,7 +2,7 @@ ;; Author: Adam Porter ;; Url: https://github.com/alphapapa/org-ql -;; Version: 0.5 +;; Version: 0.6-pre ;; Package-Requires: ((emacs "26.1") (dash "2.13") (dash-functional "1.2.0") (f "0.17.2") (map "2.1") (org "9.0") (org-super-agenda "1.2") (ov "1.0.6") (peg "0.6") (s "1.12.0") (transient "0.1") (ts "0.2-pre")) ;; Keywords: hypermedia, outlines, Org, agenda diff --git a/org-ql.info b/org-ql.info index 60d0048..abfce7a 100644 --- a/org-ql.info +++ b/org-ql.info @@ -66,6 +66,7 @@ Functions / Macros Changelog +* 0.6-pre: 06-pre. * 0.5: 05. * 0.4.9: 049. * 0.4.8: 048. @@ -831,6 +832,7 @@ releases. * Menu: +* 0.6-pre: 06-pre. * 0.5: 05. * 0.4.9: 049. * 0.4.8: 048. @@ -852,9 +854,17 @@ releases. * 0.1: 01.  -File: README.info, Node: 05, Next: 049, Up: Changelog +File: README.info, Node: 06-pre, Next: 05, Up: Changelog -5.1 0.5 +5.1 0.6-pre +=========== + +Nothing new yet. + + +File: README.info, Node: 05, Next: 049, Prev: 06-pre, Up: Changelog + +5.2 0.5 ======= *Added* @@ -895,7 +905,7 @@ File: README.info, Node: 05, Next: 049, Up: Changelog  File: README.info, Node: 049, Next: 048, Prev: 05, Up: Changelog -5.2 0.4.9 +5.3 0.4.9 ========= *Fixed* @@ -906,7 +916,7 @@ File: README.info, Node: 049, Next: 048, Prev: 05, Up: Changelog  File: README.info, Node: 048, Next: 047, Prev: 049, Up: Changelog -5.3 0.4.8 +5.4 0.4.8 ========= *Fixed* @@ -918,7 +928,7 @@ File: README.info, Node: 048, Next: 047, Prev: 049, Up: Changelog  File: README.info, Node: 047, Next: 046, Prev: 048, Up: Changelog -5.4 0.4.7 +5.5 0.4.7 ========= *Fixed* @@ -931,7 +941,7 @@ File: README.info, Node: 047, Next: 046, Prev: 048, Up: Changelog  File: README.info, Node: 046, Next: 045, Prev: 047, Up: Changelog -5.5 0.4.6 +5.6 0.4.6 ========= *Fixed* @@ -944,7 +954,7 @@ File: README.info, Node: 046, Next: 045, Prev: 047, Up: Changelog  File: README.info, Node: 045, Next: 044, Prev: 046, Up: Changelog -5.6 0.4.5 +5.7 0.4.5 ========= *Fixed* @@ -956,7 +966,7 @@ File: README.info, Node: 045, Next: 044, Prev: 046, Up: Changelog  File: README.info, Node: 044, Next: 043, Prev: 045, Up: Changelog -5.7 0.4.4 +5.8 0.4.4 ========= *Fixed* @@ -968,7 +978,7 @@ File: README.info, Node: 044, Next: 043, Prev: 045, Up: Changelog  File: README.info, Node: 043, Next: 042, Prev: 044, Up: Changelog -5.8 0.4.3 +5.9 0.4.3 ========= *Fixed* @@ -978,8 +988,8 @@ File: README.info, Node: 043, Next: 042, Prev: 044, Up: Changelog  File: README.info, Node: 042, Next: 041, Prev: 043, Up: Changelog -5.9 0.4.2 -========= +5.10 0.4.2 +========== *Fixed* • Items’ to-do keywords were not shown in views. @@ -987,7 +997,7 @@ File: README.info, Node: 042, Next: 041, Prev: 043, Up: Changelog  File: README.info, Node: 041, Next: 04, Prev: 042, Up: Changelog -5.10 0.4.1 +5.11 0.4.1 ========== *Fixed* @@ -997,7 +1007,7 @@ File: README.info, Node: 041, Next: 04, Prev: 042, Up: Changelog  File: README.info, Node: 04, Next: 032, Prev: 041, Up: Changelog -5.11 0.4 +5.12 0.4 ======== _Note:_ The next release, 0.5, may include changes which will require @@ -1078,7 +1088,7 @@ as they will be pushed to the master branch when ready.  File: README.info, Node: 032, Next: 031, Prev: 04, Up: Changelog -5.12 0.3.2 +5.13 0.3.2 ========== *Fixed* @@ -1091,7 +1101,7 @@ File: README.info, Node: 032, Next: 031, Prev: 04, Up: Changelog  File: README.info, Node: 031, Next: 03, Prev: 032, Up: Changelog -5.13 0.3.1 +5.14 0.3.1 ========== *Fixed* @@ -1101,7 +1111,7 @@ File: README.info, Node: 031, Next: 03, Prev: 032, Up: Changelog  File: README.info, Node: 03, Next: 023, Prev: 031, Up: Changelog -5.14 0.3 +5.15 0.3 ======== *Added* @@ -1166,7 +1176,7 @@ File: README.info, Node: 03, Next: 023, Prev: 031, Up: Changelog  File: README.info, Node: 023, Next: 022, Prev: 03, Up: Changelog -5.15 0.2.3 +5.16 0.2.3 ========== *Fixed* @@ -1176,7 +1186,7 @@ File: README.info, Node: 023, Next: 022, Prev: 03, Up: Changelog  File: README.info, Node: 022, Next: 021, Prev: 023, Up: Changelog -5.16 0.2.2 +5.17 0.2.2 ========== *Fixed* @@ -1187,7 +1197,7 @@ File: README.info, Node: 022, Next: 021, Prev: 023, Up: Changelog  File: README.info, Node: 021, Next: 02, Prev: 022, Up: Changelog -5.17 0.2.1 +5.18 0.2.1 ========== *Fixed* @@ -1197,7 +1207,7 @@ File: README.info, Node: 021, Next: 02, Prev: 022, Up: Changelog  File: README.info, Node: 02, Next: 01, Prev: 021, Up: Changelog -5.18 0.2 +5.19 0.2 ======== *Added* @@ -1280,7 +1290,7 @@ File: README.info, Node: 02, Next: 01, Prev: 021, Up: Changelog  File: README.info, Node: 01, Prev: 02, Up: Changelog -5.19 0.1 +5.20 0.1 ======== First tagged release. @@ -1338,53 +1348,54 @@ GPLv3  Tag Table: Node: Top225 -Node: Contents1586 -Node: Screenshots1709 -Node: Installation1827 -Node: Quelpa2465 -Node: Usage2908 -Node: Commands3306 -Node: org-ql-search3779 -Node: helm-org-ql5429 -Node: org-ql-view5841 -Node: org-ql-view-sidebar6341 -Node: org-ql-view-recent-items6697 -Node: org-ql-sparse-tree7181 -Node: Queries7981 -Node: Non-sexp query syntax9092 -Node: General predicates10799 -Node: Ancestor/descendant predicates16212 -Node: Date/time predicates17340 -Node: Functions / Macros19995 -Node: Agenda-like views20250 -Node: Listing / acting-on results21655 -Node: Dynamic block26850 -Node: Links29548 -Node: Tips30235 -Node: Changelog30553 -Node: 0531229 -Node: 04932691 -Node: 04832965 -Node: 04733312 -Node: 04633707 -Node: 04534107 -Node: 04434466 -Node: 04334823 -Node: 04235018 -Node: 04135177 -Node: 0435418 -Node: 03239351 -Node: 03139730 -Node: 0339927 -Node: 02342902 -Node: 02243130 -Node: 02143398 -Node: 0243597 -Node: 0147632 -Node: Notes47733 -Node: Comparison with Org Agenda searches47895 -Node: org-sidebar48767 -Node: License49046 +Node: Contents1606 +Node: Screenshots1729 +Node: Installation1847 +Node: Quelpa2485 +Node: Usage2928 +Node: Commands3326 +Node: org-ql-search3799 +Node: helm-org-ql5449 +Node: org-ql-view5861 +Node: org-ql-view-sidebar6361 +Node: org-ql-view-recent-items6717 +Node: org-ql-sparse-tree7201 +Node: Queries8001 +Node: Non-sexp query syntax9112 +Node: General predicates10819 +Node: Ancestor/descendant predicates16232 +Node: Date/time predicates17360 +Node: Functions / Macros20015 +Node: Agenda-like views20270 +Node: Listing / acting-on results21675 +Node: Dynamic block26870 +Node: Links29568 +Node: Tips30255 +Node: Changelog30573 +Node: 06-pre31269 +Node: 0531375 +Node: 04932852 +Node: 04833126 +Node: 04733473 +Node: 04633868 +Node: 04534268 +Node: 04434627 +Node: 04334984 +Node: 04235179 +Node: 04135340 +Node: 0435581 +Node: 03239514 +Node: 03139893 +Node: 0340090 +Node: 02343065 +Node: 02243293 +Node: 02143561 +Node: 0243760 +Node: 0147795 +Node: Notes47896 +Node: Comparison with Org Agenda searches48058 +Node: org-sidebar48930 +Node: License49209  End Tag Table From 57b3eeba49745579e0908a4a2fb54f64b2fad88c Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Mon, 23 Nov 2020 01:13:03 -0600 Subject: [PATCH 073/372] Merge: org-ql-defpred MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squashed commit of the following: commit a0ace307b0de3f5e811f042fd9f0b821a0bbc95c Author: Adam Porter Date: Mon Nov 23 01:11:07 2020 -0600 Docs: Fix TOC commit bef9f3c9b5b762070445c76ac3c833e51dd893ba Author: Adam Porter Date: Mon Nov 23 01:09:33 2020 -0600 Docs: (README.org) Add org-ql-defpred commit ff0257696fe87764e3289e12ddc336377a9a5d01 Author: Adam Porter Date: Mon Nov 23 00:52:06 2020 -0600 Docs: Improve defpred.org Thanks to piyo in #emacsconf@freenode. commit 18d6d73a99c336cd637c99309ee5d150106b73b7 Author: Adam Porter Date: Mon Nov 23 00:35:24 2020 -0600 Docs: Improve custom predicate tutorial Closes #155. Thanks to Merlin Göttlinger (@mgttlinger) for suggesting improvements. commit b8cbe36fb3bec23dad5fac07cd11018d98a0e9c1 Author: Adam Porter Date: Mon Nov 23 00:05:52 2020 -0600 Docs: Add custom predicate tutorial commit b1008d0c526b988e958809997351f347af41a7f7 Author: Adam Porter Date: Sun Nov 22 19:34:16 2020 -0600 Docs: Update readme commit 73d4f95cd4eb429774c16b6d403285e9755a2412 Merge: 9a2719f 148a193 Author: Adam Porter Date: Sun Nov 22 19:29:15 2020 -0600 Merge branch 'wip/0.6-pre' into wip/define-predicate ... --- README.org | 64 +- examples.org | 5 + examples/defpred.org | 331 ++++++++++ helm-org-ql.el | 4 +- org-ql-search.el | 4 +- org-ql-view.el | 2 +- org-ql.el | 1499 +++++++++++++++++++++++------------------- org-ql.info | 192 ++++-- tests/test-org-ql.el | 122 ++-- 9 files changed, 1432 insertions(+), 791 deletions(-) create mode 100644 examples/defpred.org diff --git a/README.org b/README.org index b4f8596..5f9e718 100644 --- a/README.org +++ b/README.org @@ -258,15 +258,19 @@ The following predicates, in addition to the keyword arguments, can also take a ** Functions / Macros :PROPERTIES: -:TOC: ignore-children +:TOC: :include descendants +:END: +:CONTENTS: +- [[#agenda-like-views][Agenda-like views]] + - [[#function-org-ql-block][Function: org-ql-block]] +- [[#listing--acting-on-results][Listing / acting-on results]] + - [[#caching][Caching]] + - [[#function-org-ql-select][Function: org-ql-select]] + - [[#function-org-ql-query][Function: org-ql-query]] + - [[#macro-org-ql][Macro: org-ql]] +- [[#custom-predicates][Custom predicates]] + - [[#macro-org-ql-defpred][Macro: org-ql-defpred]] :END: - -- [[#agenda-like-views][Agenda-like views]] - - [[#function-org-ql-block][Function: org-ql-block]] -- [[#listing--acting-on-results][Listing / acting-on results]] - - [[#function-org-ql-select][Function: org-ql-select]] - - [[#function-org-ql-query][Function: org-ql-query]] - - [[#macro-org-ql][Macro: org-ql]] *** Agenda-like views @@ -405,6 +409,44 @@ Examples: Expands into a call to ~org-ql-select~ with the same arguments. For convenience, arguments should be unquoted. +*** Custom predicates + ++ See: [[file:examples/defpred.org][Custom predicate tutorial]] + +**** Macro: =org-ql-defpred= + +/Arguments:/ ~(name args docstring &key body preambles normalizers)~ + +Define an ~org-ql~ selector predicate named ~org-ql--predicate-NAME~. ~NAME~ may be a symbol or a list of symbols: if a list, the first is used as ~NAME~ and the rest are aliases. ~A~ function is only created for ~NAME~, not for aliases, so a normalizer should be used to replace aliases with ~NAME~ in queries (keep reading). + +~ARGS~ is a ~cl-defun~-style argument list. ~DOCSTRING~ is the function's docstring. + +~BODY~ is the body of the predicate. It will be evaluated with point on the beginning of an Org heading and should return non-nil if the heading's entry is a match. + +~PREAMBLES~ and ~NORMALIZERS~ are lists of ~pcase~ forms matched against Org ~QL~ query sexps. They are spliced into ~pcase~ forms in the definitions of the functions ~org-ql--query-preamble~ and ~org-ql--normalize-query~, which see. Those functions are redefined when this macro is expanded, unless variable ~org-ql-defpred-defer~ is non-nil, in which case those functions should be redefined manually after defining predicates by calling ~org-ql--define-query-preamble-fn~ and ~org-ql--define-normalize-query-fn~. + +~NORMALIZERS~ are used to normalize query expressions to standard forms. For example, when the predicate has aliases, the aliases should be replaced with predicate names using a normalizer. Also, predicate arguments may be put into a more optimal form so that the predicate has less work to do at query time. + +~PREAMBLES~ refer to regular expressions which may be used to search through a buffer directly to a potential match rather than testing the predicate body on each heading. (Naming things is hard.) In each ~pcase~ form in ~PREAMBLES~, the ~pcase~ expression (not the pattern) should be a plist with the following keys, each value of which should be an expression which may refer to variables bound in the pattern: + +~:regexp~ Regular expression which searches directly to a potential match. + +~:case-fold~ Bound to ~case-fold-search~ around the regexp search. + +~:query~ Expression which should replace the query expression, or ~query~ if it should not be changed (e.g. if the regexp is insufficient to determine whether a heading matches, in which case the predicate's body needs to be tested on the heading). If the regexp guarantees a match, this may be simply ~t~, leaving the query expression with no work to do, which improves performance. + +For convenience, within the ~pcase~ patterns, the symbol ~predicate-names~ is a special form which is replaced with a pattern matching any of the predicate's name and aliases. For example, if ~NAME~ were: + +~(heading h)~ + +Then if ~NORMALIZERS~ were: + +~((`(,predicate-names . ,args) `(heading ,@args)))~ + +It would be expanded to: + +~((`(,(or 'heading 'h) . ,args) `(heading ,@args)))~ + ** Dynamic block Org QL provides a dynamic block that lists entries in the current document matching a query. In the header, these parameters are supported: @@ -463,7 +505,11 @@ Simple links may also be written manually in either sexp or non-sexp form, like: ** 0.6-pre -Nothing new yet. +*Added* ++ Macro =org-ql-defpred=, used to define search predicates. (See [[file:examples/defpred.org][tutorial]].) + +*Internal* ++ Predicates are now defined more cleanly with a macro (=org-ql-defpred=) that consolidates functionality related to each predicate. This will also allow users to more easily define custom predicates. ** 0.5 diff --git a/examples.org b/examples.org index eb14f08..7d5f28d 100644 --- a/examples.org +++ b/examples.org @@ -5,6 +5,7 @@ :TOC: :include siblings :ignore this :END: :CONTENTS: +- [[#custom-predicates][Custom predicates]] - [[#agenda-like-view][Agenda-like view]] - [[#entries-from-the-past-week][Entries from the past week]] - [[#find-entries-matching-a-certain-custom_id][Find entries matching a certain CUSTOM_ID]] @@ -18,6 +19,10 @@ - [[#task-list-for-files-in-subdirectories][Task list for files in subdirectories]] :END: +* Custom predicates + ++ [[file:examples/defpred.org][Custom predicate tutorial]] + * Agenda-like view Show an agenda-like view, similar to a "traditional" Org Agenda with Log Mode turned on. diff --git a/examples/defpred.org b/examples/defpred.org new file mode 100644 index 0000000..86565cf --- /dev/null +++ b/examples/defpred.org @@ -0,0 +1,331 @@ +#+TITLE: Org QL Custom Predicates Tutorial +#+OPTIONS: author:nil creator:nil created:nil date:nil num:nil title:t + +[2020-11-22 Sun 22:35] Imagine you have weekly meetings with other people, and during the week you take notes about items to discuss at each meeting. When the time for a meeting comes, you want to quickly and easily search for all of the items to discuss at the meeting. + +You've been experimenting with different ways to track such data in Org. You've tried using tags, but some of the names in question conflict with other tags in your data (e.g. someone's named Charles, but you also work with a firm named Charles, Inc., and you'd prefer to continue using the tag =Charles= for entries about that firm), so you've been using tags like ~:personNAME:~, which seems awkward. You've tried using a ~:person: NAME~ property on entries, which has the advantage of not cluttering the tags list, but also the disadvantage of not being readily visible in an outline. + +So you haven't decided on a long-term solution, but the meetings aren't going to wait--you need to search that data now, and you have a mix of both tags and properties in your entries. What you need is to be able to search for all of the entries about Alice (which you've tagged ~:personAlice:~) when you're meeting with her, and all of the entries about Bob (which have the property ~:person: Bob~) when you're meeting with him What do you do? + +* Contents +:PROPERTIES: +:TOC: :include siblings :ignore this +:END: +:CONTENTS: +- [[#using-built-in-predicates][Using built-in predicates]] +- [[#a-custom-person-predicate][A custom (person) predicate]] +- [[#searching-for-multiple-people-at-once][Searching for multiple people at once]] +- [[#normalizing-queries-to-rewrite-arguments][Normalizing queries to rewrite arguments]] +- [[#non-sexp-query-syntax][Non-sexp query syntax]] +- [[#using-multiple-predicates][Using multiple predicates]] +- [[#predicate-aliases][Predicate aliases]] +- [[#appendix-anaphoric-macros][Appendix: Anaphoric macros]] +:END: + +* Using built-in predicates + +You could start by using built-in Org QL predicates to search your data. For example: + +#+BEGIN_SRC elisp :results list :exports both :cache yes + (org-ql-query :select '(org-get-heading :no-tags) + :from (current-buffer) + :where '(or (tags "personAlice") + (property "person" "Bob"))) +#+END_SRC + +#+RESULTS[91a413cda23cb65d6bb99212e111f283e5a5c910]: +- [#A] Loud pet parakeet +- [#C] Missing sticky notes +- [#C] Dirty dishes in sink +- [#A] Stinky coffee breath + +That was easy enough, but it's not very...semantic. You have to think about the implementation details: Alice uses tags, Bob uses properties, and what if Charlie uses both? It starts to feel complicated, and it's a lot to type out every time. Is there an easier way? + +* A custom ~(person)~ predicate + +Enter =org-ql= custom search predicates. Let's start simple, by defining a predicate to search for just the ~:person:~ property, one person at a time. The predicate will take one argument, a person's name, and search for that property. It would look like this: + +#+BEGIN_SRC elisp :results silent :exports code + (org-ql-defpred person (name) + "Search for entries with the \"person\" property being NAME." + :body (property "person" name)) +#+END_SRC + +Now let's see what results we get for searching this file for entries about Bob: + +#+BEGIN_SRC elisp :results list :exports both :cache yes + (org-ql-query :select '(org-get-heading :no-tags) + :from (current-buffer) + :where '(person "Bob")) +#+END_SRC + +#+RESULTS[c11a4ce2c4f179d7487c9b46eff9f72766bc2bc4]: +- [#C] Missing sticky notes +- [#C] Dirty dishes in sink +- [#A] Stinky coffee breath + +Hmm, looks like we need to remind Bob to wash his mug and take some mints after lunch. Now what do we need to discuss with Alice? + +#+BEGIN_SRC elisp :results list :exports both :cache yes + (org-ql-query :select '(org-get-heading :no-tags) + :from (current-buffer) + :where '(person "Alice")) +#+END_SRC + +#+RESULTS[1f12f437042bbc077a4696d707805c1367f2ca3d]: +#+BEGIN_EXAMPLE + +#+END_EXAMPLE + +Nothing? Oh, right, Alice's entries use the ~:personAlice:~ tag, so we'll also need to also search those kind of entries. Let's make the predicate do that too: + +#+BEGIN_SRC elisp :results silent :exports code + (org-ql-defpred person (name) + "Search for entries with the \"person\" property being NAME or having the tag \"personNAME\"." + :body (or (property "person" name) + (tags (concat "person" name)))) +#+END_SRC + +How about now? + +#+BEGIN_SRC elisp :results list :exports both :cache yes + (org-ql-query :select '(org-get-heading :no-tags) + :from (current-buffer) + :where '(person "Alice")) +#+END_SRC + +#+RESULTS[1f12f437042bbc077a4696d707805c1367f2ca3d]: +- [#A] Loud pet parakeet +- [#C] Missing sticky notes +- [#C] Dirty dishes in sink + +Hmm, I thought we already told her to leave Polly at home. + +* Searching for multiple people at once + +Oh, wait, this week is shortened due to holidays, so we're having a combined meeting. How do we search for entries about either of them? Well, this is the obvious solution: + +#+BEGIN_SRC elisp :results list :exports both :cache yes + (org-ql-query :select '(org-get-heading :no-tags) + :from (current-buffer) + :where '(or (person "Alice") + (person "Bob"))) +#+END_SRC + +#+RESULTS[4e4c75bde4fbceaadb076a53410c1625d1283e06]: +- [#A] Loud pet parakeet +- [#C] Missing sticky notes +- [#C] Dirty dishes in sink +- [#A] Stinky coffee breath + +And that works fine. But it seems like a lot to type. Could we make the =person= predicate accept multiple names instead? + +#+BEGIN_SRC elisp :results silent :exports code + (org-ql-defpred person (&rest names) + "Search for entries about any of NAMES." + :body (cl-loop for name in names + thereis (or (property "person" name) + (tags (concat "person" name))))) +#+END_SRC + +#+BEGIN_SRC elisp :results list :exports both :cache yes + (org-ql-query :select '(org-get-heading :no-tags) + :from (current-buffer) + :where '(person "Alice" "Bob")) +#+END_SRC + +#+RESULTS[4f5971c56616f01d8d3c28a66ef380495ee3e158]: +- [#A] Loud pet parakeet +- [#C] Missing sticky notes +- [#C] Dirty dishes in sink +- [#A] Stinky coffee breath + +That was easy! + +* Normalizing queries to rewrite arguments + +Now, all this is well and good if you don't have hundreds of thousands of Org entries in your files. But what if you do? All that =concat='ing happening on every entry could add up, and the query might take a few seconds. What if we could do that stringing-along just once, before running the query? We want to turn our ~(person "Alice" "Bob")~ query into this, with the =:personNAME:= strings already made and the per-person ~(property ...)~ predicates also included: + +#+BEGIN_SRC elisp + (or (tags "personAlice" "personBob") + (property "person" "Alice") + (property "person" "Bob")) +#+END_SRC + +Can we do that? In fact, we can, by using a query normalizer. Normalizers are =pcase= forms (I /know/) that normalize query expressions before execution. We can use one to rewrite the query ahead of time, like this: + +#+BEGIN_SRC elisp :results silent :exports code + (org-ql-defpred person (&rest names) + "Search for entries about any of NAMES." + :normalizers ((`(person . ,names) + `(or (tags ,@(cl-loop for name in names + collect (concat "person" name))) + ,@(cl-loop for name in names + collect `(property "person" ,name))))) + :body (cl-loop for name in names + thereis (or (property "person" name) + (tags (concat "person" name))))) +#+END_SRC + +Now, don't faint from all the backquoting and unquoting--it's just Lisp, nothing to be afraid of! Let's slow down a moment and see what the normalized query looks like to be sure we're doing it correctly: + +#+BEGIN_SRC elisp :results code :exports both :cache yes + (org-ql--normalize-query '(person "Alice" "Bob")) +#+END_SRC + +#+RESULTS[ebc46fff31b72359353dda539a26c95b7d650df2]: +#+BEGIN_SRC elisp + (or (tags "personAlice" "personBob") + (property "person" "Alice") + (property "person" "Bob")) +#+END_SRC + +And, as they say, Bob's your uncle! Or even if he isn't, let's see if it works: + +#+BEGIN_SRC elisp :results list :exports both :cache yes + (org-ql-query :select '(org-get-heading :no-tags) + :from (current-buffer) + :where '(person "Alice" "Bob")) +#+END_SRC + +#+RESULTS[4f5971c56616f01d8d3c28a66ef380495ee3e158]: +- [#A] Loud pet parakeet +- [#C] Missing sticky notes +- [#C] Dirty dishes in sink +- [#A] Stinky coffee breath + +Yep, same result as the non-normalized query. And look at how much simpler it is to write ~(person "Alice" "Bob")~ than to write ~(or (tags "personAlice" "personBob") (property "person" "Alice") (property "person" "Bob"))~. + +* Non-sexp query syntax + +But wait, that's not all! If you order now, we'll throw in non-sexp query syntax for free! That's right, your search could be as simple as typing ~person:Alice,Bob~! + +#+BEGIN_SRC elisp :results none :exports code + (org-ql-search (current-buffer) "person:Alice,Bob") +#+END_SRC + +Don't believe me? Well, you see, queries in this syntax are automatically converted to the sexp syntax, like: + +#+BEGIN_SRC elisp :results code :exports both :cache yes + (org-ql--query-string-to-sexp "person:Alice,Bob") +#+END_SRC + +#+RESULTS[a60655544956644605c23c152570185c329faa87]: +#+BEGIN_SRC elisp + (person "Alice" "Bob") +#+END_SRC + +But that happens automatically when you use a search command like =org-ql-search=. If you have =org-ql= installed already, you could even click this link: [[org-ql-search:person:Alice,Bob][Alice or Bob]]. Which, in Org syntax, looks like: + +#+BEGIN_SRC org + [[org-ql-search:person:Alice,Bob]] +#+END_SRC + +And that would open an Agenda Mode buffer that looks like this: + +#+BEGIN_EXAMPLE + Query: (person "Alice" "Bob") In:meetings.org + [#A] Loud pet parakeet :personAlice: + [#C] Missing sticky notes :personAlice: + [#C] Dirty dishes in sink :personAlice: + [#A] Stinky coffee breath +#+END_EXAMPLE + +* Using multiple predicates + +Oops, you forgot that there's a birthday party in 20 minutes, so you only have time to talk about the highest priority items at this joint meeting today. + +No problem, let's just select high-priority items: + +#+BEGIN_SRC elisp :results silent :exports code + (org-ql-search (current-buffer) "person:Alice,Bob priority:A") +#+END_SRC + +#+BEGIN_EXAMPLE + Query: (and (person "Alice" "Bob") (priority "A")) In:meetings.org + [#A] Loud pet parakeet :personAlice: + [#A] Stinky coffee breath +#+END_EXAMPLE + +* Predicate aliases + +And, you know what, if you're just so busy that you don't even have time to type the word =person=, you can add an abbreviated alias, =p=, like this: + +#+BEGIN_SRC elisp :results silent :exports code + (org-ql-defpred (person p) (&rest names) + "Search for entries about any of NAMES." + :normalizers ((`(,predicate-names . ,names) + `(or (tags ,@(cl-loop for name in names + collect (concat "person" name))) + ,@(cl-loop for name in names + collect `(property "person" ,name))))) + :body (cl-loop for name in names + thereis (or (property "person" name) + (tags (concat "person" name))))) +#+END_SRC + +#+BEGIN_SRC elisp :results silent :exports code + (org-ql-search (current-buffer) "p:Alice,Bob priority:A") +#+END_SRC + +#+BEGIN_EXAMPLE + Query: (and (person "Alice" "Bob") (priority "A")) In:meetings.org + [#A] Loud pet parakeet :personAlice: + [#A] Stinky coffee breath +#+END_EXAMPLE + +(It's up to you to remember whether =p= means =person= or =priority=, but code can't solve everything.) + +* Appendix: Anaphoric macros + +Finally, if you're a Lisper who appreciates anaphora, you might prefer a more syntactically concise definition of the predicate using Dash macros: + +#+BEGIN_SRC elisp :results silent :exports code + (org-ql-defpred (person p) (&rest names) + "Search for entries about any of NAMES." + :normalizers ((`(,predicate-names . ,names) + `(or (tags ,@(--map `(concat "person" ,it) names)) + ,@(--map `(property "person" ,it) names)))) + :body (--any (or (property "person" name) + (tags (concat "person" name))) + names)) +#+END_SRC + +Let's make sure it works: + +#+BEGIN_SRC elisp :results list :exports both :cache yes + (org-ql-query :select '(org-get-heading :no-tags) + :from (current-buffer) + :where '(person "Alice" "Bob")) +#+END_SRC + +#+RESULTS[4f5971c56616f01d8d3c28a66ef380495ee3e158]: +- [#A] Loud pet parakeet +- [#C] Missing sticky notes +- [#C] Dirty dishes in sink +- [#A] Stinky coffee breath + +Have fun making custom search predicates! + +* Example data +:PROPERTIES: +:TOC: :ignore (this descendants) +:END: + +** [#A] Loud pet parakeet :personAlice: + +** [#C] Missing sticky notes :personAlice: +:PROPERTIES: +:person: Bob +:END: + +** [#C] Dirty dishes in sink :personAlice: +:PROPERTIES: +:person: Bob +:END: + +** [#A] Stinky coffee breath +:PROPERTIES: +:person: Bob +:END: + diff --git a/helm-org-ql.el b/helm-org-ql.el index e95bc5f..e75cf7b 100644 --- a/helm-org-ql.el +++ b/helm-org-ql.el @@ -173,7 +173,7 @@ Is transformed into this query: (interactive) (let ((buffers-files (with-current-buffer (helm-buffer-get) helm-org-ql-buffers-files)) - (query (org-ql--plain-query helm-pattern))) + (query (org-ql--query-string-to-sexp helm-pattern))) (helm-run-after-exit #'org-ql-search buffers-files query))) ;;;###autoload @@ -189,7 +189,7 @@ Is transformed into this query: ;; Expansion of `helm-build-sync-source' macro. (helm-make-source name 'helm-source-sync :candidates (lambda () - (let* ((query (org-ql--plain-query helm-pattern)) + (let* ((query (org-ql--query-string-to-sexp helm-pattern)) (window-width (window-width (helm-window)))) (when query (with-current-buffer (helm-buffer-get) diff --git a/org-ql-search.el b/org-ql-search.el index 70d846d..c60bd8e 100644 --- a/org-ql-search.el +++ b/org-ql-search.el @@ -158,7 +158,7 @@ necessary." ;; Read sexp query. (read query) ;; Parse non-sexp query into sexp query. - (org-ql--plain-query query))) + (org-ql--query-string-to-sexp query))) (list query))) (results (org-ql-select buffers-files query :action 'element-with-markers @@ -273,7 +273,7 @@ For example, an org-ql dynamic block header could look like: #+BEGIN: org-ql :query (todo \"UNDERWAY\") :columns (priority todo heading) :sort (priority date) :ts-format \"%Y-%m-%d %H:%M\"" (-let* (((&plist :query :columns :sort :ts-format :take) params) (query (cl-etypecase query - (string (org-ql--plain-query query)) + (string (org-ql--query-string-to-sexp query)) (list ;; SAFETY: Query is in sexp form: ask for confirmation, because it could contain arbitrary code. (org-ql--ask-unsafe-query query) query))) diff --git a/org-ql-view.el b/org-ql-view.el index b954fc0..959484c 100644 --- a/org-ql-view.el +++ b/org-ql-view.el @@ -775,7 +775,7 @@ When opened, the link searches the buffer it's opened from." ;; Read sexp query. (read query) ;; Parse non-sexp query into sexp query. - (org-ql--plain-query query))))) + (org-ql--query-string-to-sexp query))))) (define-infix-command org-ql-view--transient-in () :description (lambda () (org-ql-view--format-transient-lisp-argument "In buffers/files" org-ql-view-buffers-files)) diff --git a/org-ql.el b/org-ql.el index bd53108..b4b98b3 100644 --- a/org-ql.el +++ b/org-ql.el @@ -125,7 +125,9 @@ the value returned by it at that node.") (eval-and-compile (defvar org-ql-predicates - (list (list :name 'org-back-to-heading :fn (symbol-function 'org-back-to-heading))) + ;; FIXME: Is this remapping still necessary? It was mapping `org-back-to-heading' + ;; to itself until now, so maybe I broke it and it doesn't matter anymore. + (list (cons 'org-back-to-heading (list :name 'org-back-to-heading :fn (symbol-function 'outline-back-to-heading)))) "Plist of predicates, their corresponding functions, and their docstrings. This list should not contain any duplicates.")) @@ -254,7 +256,7 @@ returns nil or non-nil." (user-error "Can't open file: %s" it))))) ;; Ignore special/hidden buffers. (--remove (string-prefix-p " " (buffer-name it))))) - (query (org-ql--pre-process-query query)) + (query (org-ql--normalize-query query)) ((&plist :query :preamble :preamble-case-fold) (org-ql--query-preamble query)) (predicate (org-ql--query-predicate query)) (action (pcase action @@ -377,13 +379,15 @@ If NARROW is non-nil, buffer will not be widened." (let (orig-fns) (--each org-ql-predicates ;; Save original function mappings. - (let ((name (plist-get it :name))) + (let* ((it (cdr it)) + (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))) + (let ((it (cdr it))) + (fset (plist-get it :name) (plist-get it :fn)))) ;; Run query. (save-excursion (save-restriction @@ -586,356 +590,6 @@ Or, when possible, fix the problem." (org-ql--sanity-check-form (cdr elem))) else do (check elem)))) -(defun org-ql--pre-process-query (query) - "Return QUERY having been pre-processed. -Replaces bare strings with (regexp) selectors, and appropriate -`ts'-related selectors." - ;; This is unsophisticated, but it works. - ;; TODO: Maybe query pre-processing should be done in one place, - ;; rather than here and in --query-predicate. - ;; NOTE: Don't be scared by the `pcase' patterns! They make this - ;; all very easy once you grok the backquoting and unquoting. - (cl-labels ((rec (element) - (pcase element - (`(or . ,clauses) `(or ,@(mapcar #'rec clauses))) - (`(and . ,clauses) `(and ,@(mapcar #'rec clauses))) - (`(not . ,clauses) `(not ,@(mapcar #'rec clauses))) - (`(when ,condition . ,clauses) `(when ,(rec condition) - ,@(mapcar #'rec clauses))) - (`(unless ,condition . ,clauses) `(unless ,(rec condition) - ,@(mapcar #'rec clauses))) - ;; TODO: Combine (regexp) when appropriate (i.e. inside an OR, not an AND). - ((pred stringp) `(regexp ,element)) - ;; Quote children queries so the user doesn't have to. - (`(children ,query) `(children ',query)) - (`(children) '(children (lambda () t))) - (`(descendants ,query) `(descendants ',query)) - (`(descendants) '(descendants (lambda () t))) - (`(parent ,query) `(parent ,(org-ql--query-predicate (rec query)))) - (`(parent) '(parent (lambda () t))) - (`(ancestors ,query) `(ancestors ,(org-ql--query-predicate (rec query)))) - (`(ancestors) '(ancestors (lambda () t))) - ;; Timestamp-based predicates. I think this is the way that makes the most sense: - ;; set the limit to N days in the future, adjusted to 23:59:59 (since Org doesn't - ;; support timestamps down to the second, anyway, there should be no need to adjust - ;; it forward to 00:00:00 of the next day). That way, e.g. if it's Monday at 3 PM, - ;; and N is 1, rather than showing items up to 3 PM Tuesday, it will show items any - ;; time on Tuesday. If this isn't desired, the user can pass a specific timestamp. - (`(,(and pred (or 'clocked 'closed)) - ,(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)))) - `(,pred :from ,from))) - (`(deadline 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))) - (`(,(and pred (or 'deadline 'scheduled 'planning)) - ,(and num-days (pred numberp))) - (let ((to (->> (ts-now) - (ts-adjust 'day num-days) - (ts-apply :hour 23 :minute 59 :second 59)))) - `(,pred :to ,to))) - - ;; Headings. - (`(h . ,args) - ;; "h" alias. - `(heading ,@args)) - - ;; Outline level. - (`(level . ,args) - ;; Arguments could be given as strings (e.g. from a non-Lisp query). - `(level ,@(--map (pcase it - ((or "<" "<=" ">" ">=" "=") - (intern it)) - ((pred stringp) (string-to-number it)) - (_ it)) - args))) - - ;; Regexps. - (`(r . ,args) - ;; "r" alias. - `(regexp ,@args)) - - ;; Outline paths. - (`(,(or 'outline-path 'olp) . ,strings) - ;; Regexp quote headings. - `(outline-path ,@(mapcar #'regexp-quote strings))) - (`(,(or 'outline-path-segment 'olps) . ,strings) - ;; Regexp quote headings. - `(outline-path-segment ,@(mapcar #'regexp-quote strings))) - - ;; Priorities - (`(priority ,(and (or '= '< '> '<= '>=) comparator) ,letter) - ;; Quote comparator. - `(priority ',comparator ,letter)) - - ;; Properties. - (`(property ,property . ,value) - ;; Convert keyword property arguments to strings. Non-sexp - ;; queries result in keyword property arguments (because to do - ;; otherwise would require ugly special-casing in the parsing). - (when (keywordp property) - (setf property (substring (symbol-name property) 1))) - (cons 'property (cons property value))) - - ;; Source blocks. - (`(src . ,args) - ;; Rewrite to use keyword args. - (-let (regexps lang keyword-index) - (cond ((plist-get args :lang) - ;; Lang given first, or only lang given. - (setf lang (plist-get args :lang) - regexps (seq-difference args (list :lang lang)))) - ((setf keyword-index (-find-index #'keywordp args)) - ;; Regexps and lang given. - (setf lang (plist-get (cl-subseq args keyword-index) :lang) - regexps (cl-subseq args 0 keyword-index))) - (t ;; Only regexps given. - (setf regexps args))) - (when regexps - ;; This feels awkward and wrong, but we have to quote lists - ;; and avoid quoting nil. There must be a better way. - (setf regexps `(',regexps))) - `(src :lang ,lang :regexps ,@regexps))) - - ;; Tags. - (`(,(or 'tags-all 'tags&) . ,tags) `(and ,@(--map `(tags ,it) tags))) - ;; MAYBE: -all versions for inherited and local. - ;; Inherited and local predicate aliases. - (`(,(or 'tags-i 'itags 'inherited-tags) . ,tags) `(tags-inherited ,@tags)) - (`(,(or 'tags-l 'ltags 'local-tags) . ,tags) `(tags-local ,@tags)) - (`(,(or 'tags*) . ,regexps) `(tags-regexp ,@regexps)) - - ;; Timestamps - (`(,(or 'ts-active 'ts-a) . ,rest) `(ts :type active ,@rest)) - (`(,(or 'ts-inactive 'ts-i) . ,rest) `(ts :type inactive ,@rest)) - ;; Any other form: passed through unchanged. - (_ element)))) - (rec query))) - -(defun org-ql--query-preamble (query) - "Return plist (QUERY PREAMBLE PREAMBLE-CASE-FOLD) for QUERY. -When QUERY has a clause with a corresponding preamble, and it's -appropriate to use one (i.e. the clause is not in an `or'), -replace the clause with a preamble." - (pcase org-ql-use-preamble - ('nil (list :query query :preamble nil)) - (_ (let ((preamble-case-fold t) - org-ql-preamble) - (cl-labels ((rec (element) - (or (when org-ql-preamble - ;; Only one preamble is allowed - element) - (pcase element - (`(or _) element) - (`(clocked . ,_) - (setq org-ql-preamble org-ql-clock-regexp) - element) - (`(closed . ,_) - (setq org-ql-preamble org-closed-time-regexp) - ;; Return element, because the predicate still needs testing. - element) - (`(deadline . ,_) - (setq org-ql-preamble org-deadline-time-regexp) - ;; Return element, because the predicate still needs testing. - element) - (`(regexp . ,regexps) - ;; Search for first regexp, then confirm with predicate. - (setq org-ql-preamble (car regexps)) - element) - (`(todo . ,(and todo-keywords (guard todo-keywords))) - (setf org-ql-preamble - (rx-to-string `(seq bol (1+ "*") (1+ space) (or ,@todo-keywords) (or " " eol)) - t) - preamble-case-fold nil) - ;; Return nil, don't test the predicate. - nil) - (`(habit) - (setq org-ql-preamble (rx bol (0+ space) ":STYLE:" (1+ space) "habit" (0+ space) eol)) - nil) - - ;; Heading text. - ;; MAYBE: Adjust regexp to avoid matching in tag list. - (`(heading ,regexp) - ;; Only one regexp: match with preamble, then let predicate confirm (because - ;; the match could be in e.g. the tags rather than the heading text). - (setq org-ql-preamble (rx-to-string `(seq bol (1+ "*") (1+ blank) (0+ nonl) - ,regexp) - 'no-group)) - element) - (`(heading . ,regexps) - ;; Multiple regexps: use preamble to match against first - ;; regexp, then let the predicate match the rest. - (setq org-ql-preamble (rx-to-string `(seq bol (1+ "*") (1+ blank) (0+ nonl) - ,(car regexps)) - 'no-group)) - element) - - ;; Heading levels. - (`(level ,comparator-or-num ,num) - (let ((repeat (pcase comparator-or-num - ('< `(repeat 1 ,(1- num) "*")) - ('<= `(repeat 1 ,num "*")) - ('> `(>= ,(1+ num) "*")) - ('>= `(>= ,num "*")) - ((pred integerp) `(repeat ,comparator-or-num ,num "*"))))) - (setq org-ql-preamble (rx-to-string `(seq bol ,repeat " ") t)) - ;; Return nil, because we don't need to test the predicate. - nil)) - (`(level ,num) - (setq org-ql-preamble (rx-to-string `(seq bol (repeat ,num "*") " ") t)) - nil) - - ;; Links. Always return nil, because we - ;; shouldn't need to test the predicate. - (`(link) - (setq org-ql-preamble - ;; Match a link with a target and optionally a description. - (rx (or bol (1+ blank)) - "[[" (1+ (not (any "]"))) "]" - (optional (seq "[" (0+ (not (any "]"))) "]")) - "]" - (or eol blank))) - nil) - ;; NOTE: I would use the form "(map :regexp-p)", or at least - ;; "(map (:regexp-p regexp))" but they require map versions from - ;; ELPA. That would be fine, except that I can't automatically - ;; install those versions with makem.sh into a sandbox, because - ;; `package-install' doesn't accept a version argument. So I - ;; have to use `plist-get' here for now. Maybe when we drop - ;; support for Emacs <28... - (`(link ,(and description-or-target - (guard (not (keywordp description-or-target))))) - (setq org-ql-preamble - (org-ql--link-regexp :description-or-target - (regexp-quote description-or-target))) - nil) - (`(link . ,plist) - (setq org-ql-preamble - (org-ql--link-regexp - :description - (when (plist-get plist :description) - (regexp-quote (plist-get plist :description))) - :target (when (plist-get plist :target) - (regexp-quote (plist-get plist :target))))) - nil) - - ;; Planning lines. - (`(planning . ,_) - (setq org-ql-preamble org-ql-planning-regexp) - ;; Return element, because the predicate still needs testing. - element) - - ;; Priorities. - ;; NOTE: This only accepts A, B, or C. I haven't seen - ;; other priorities in the wild, so this will do for now. - (`(priority) - ;; Any priority cookie. - (setq org-ql-preamble (rx-to-string `(seq bol (1+ "*") (1+ blank) (0+ nonl) "[#" (in "ABC") "]") t)) - nil) - (`(priority ,(and (or ''= ''< ''> ''<= ''>=) comparator) ,letter) - ;; Comparator and priority letter. - ;; NOTE: The double-quoted comparators. See below. - (let* ((priority-letters '("A" "B" "C")) - (index (-elem-index letter priority-letters)) - ;; NOTE: Higher priority == lower number. - ;; NOTE: Because we need to support both preamble-based queries and - ;; regular predicate ones, we work around an idiosyncrasy of query - ;; pre-processing by accepting both quoted and double-quoted comparator - ;; function symbols. Not the most elegant solution, but it works. - (priorities (s-join "" (pcase comparator - ((or '= ''=) (list letter)) - ((or '> ''>) (cl-subseq priority-letters 0 index)) - ((or '>= ''>=) (cl-subseq priority-letters 0 (1+ index))) - ((or '< ''<) (cl-subseq priority-letters (1+ index))) - ((or '<= ''<=) (cl-subseq priority-letters index)))))) - (setq org-ql-preamble (rx-to-string `(seq bol (1+ "*") (1+ blank) (optional (1+ upper) (1+ blank)) - "[#" (in ,priorities) "]") t)) - nil)) - (`(priority . ,letters) - ;; One or more priorities. - ;; MAYBE: Disable case-folding. - (setq org-ql-preamble (rx-to-string `(seq bol (1+ "*") (1+ blank) - (optional (1+ upper) (1+ blank)) - "[#" (or ,@letters) "]") t)) - nil) - - ;; Properties. - ;; MAYBE: Should case folding be disabled for properties? What about values? - (`(property ,property ,value) - ;; We do NOT return nil, because the predicate still needs to be tested, - ;; because the regexp could match a string not inside a property drawer. - (setq org-ql-preamble (rx-to-string `(seq bol (0+ space) ":" ,property ":" - (1+ space) ,value (0+ space) eol))) - element) - (`(property ,property) - ;; We do NOT return nil, because the predicate still needs to be tested, - ;; because the regexp could match a string not inside a property drawer. - ;; NOTE: The preamble only matches if there appears to be a value. - ;; A line like ":ID: " without any other text does not match. - (setq org-ql-preamble (rx-to-string `(seq bol (0+ space) ":" ,property ":" (1+ space) - (minimal-match (1+ not-newline)) eol))) - element) - ;; MAYBE: Support (property) without args. - ;; (`(property) - ;; ;; We do NOT return nil, because the predicate still needs to be tested, - ;; ;; because the regexp could match a string not inside a property drawer. - ;; ;; NOTE: The preamble only matches if there appears to be a value. - ;; ;; A line like ":ID: " without any other text does not match. - ;; (setq org-ql-preamble (rx-to-string `(seq bol (0+ space) ":" (1+ (not (or space ":"))) ":" - ;; (1+ space) (minimal-match (1+ not-newline)) eol))) - ;; element) - - ;; Src blocks. - (`(src . ,args) - (setq org-ql-preamble (org-ql--format-src-block-regexp (plist-get args :lang))) - ;; Always check contents with predicate. - element) - - ;; Scheduled. - (`(scheduled . ,_) - (setq org-ql-preamble org-scheduled-time-regexp) - ;; Return element, because the predicate still needs testing. - element) - - ;; Tags. - (`((or 'tags-local 'local-tags 'tags-l 'ltags) . ,tags) - ;; When searching for local, non-inherited tags, we can - ;; search directly to headings containing one of the tags. - (setq org-ql-preamble (rx-to-string `(seq bol (1+ "*") (1+ space) (1+ not-newline) - ":" (or ,@tags) ":") - t)) - ;; Return nil, because we don't need to test the predicate. - nil) - - ;; Timestamps. - (`(ts . ,rest) - (setq org-ql-preamble (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. - (-let (((&keys :from :to :on) rest)) - (when (or from to on) - element))) - (`(and . ,rest) - (let ((clauses (mapcar #'rec rest))) - `(and ,@(-non-nil clauses)))) - (_ element))))) - (setq query (pcase (mapcar #'rec (list query)) - ((or `(nil) - `((nil)) - `((and)) - `((or))) - t) - (query (-flatten-n 1 query)))) - (list :query query :preamble org-ql-preamble :preamble-case-fold preamble-case-fold)))))) - (cl-defun org-ql--link-regexp (&key description-or-target description target) "Return a regexp matching Org links according to arguments. Each argument is treated as a regexp (so non-regexp strings @@ -1000,6 +654,351 @@ respectively." "#+end_src") t)) +(defun org-ql--byte-compile-warning (_string _pos _fill level) + "Signal an `org-ql-invalid-query' error. +Arguments STRING, POS, FILL, and LEVEL are according to +`byte-compile-log-warning-function'." + ;; Used as the `byte-compile-log-warning-function' in `org-ql--query-preamble'. + (signal 'org-ql-invalid-query level)) + +(defun org-ql--query-predicate (query) + "Return predicate function for QUERY." + ;; Use custom log function to prevent warnings for e.g. partially typed queries. + (let ((byte-compile-log-warning-function #'org-ql--byte-compile-warning)) + (byte-compile + `(lambda () + (cl-macrolet ((clocked (&key from to on) + (org-ql--from-to-on) + `(org-ql--predicate-clocked :from ,from :to ,to)) + (closed (&key from to on) + (org-ql--from-to-on) + `(org-ql--predicate-closed :from ,from :to ,to)) + (deadline (&key from to on) + (org-ql--from-to-on) + `(org-ql--predicate-deadline :from ,from :to ,to)) + (planning (&key from to on) + (org-ql--from-to-on) + `(org-ql--predicate-planning :from ,from :to ,to)) + (scheduled (&key from to on) + (org-ql--from-to-on) + `(org-ql--predicate-scheduled :from ,from :to ,to)) + (ts (&key from to on (type 'both)) + (org-ql--from-to-on) + `(org-ql--predicate-ts :from ,from :to ,to + :regexp ,(pcase type + ('both org-tsr-regexp-both) + ('active org-tsr-regexp) + ('inactive org-ql-tsr-regexp-inactive))))) + ,query))))) + +;;;;; String query parsing + +;; This section implements parsing of "plain," non-Lisp queries using the `peg' +;; library. NOTE: This needs to appear after the predicates are defined. + +(require 'peg) + +;; Fix compiler warnings probably caused by `peg' not using lexical-binding. +;; TODO: File bug report upstream. +(defvar peg-errors nil) +(defvar peg-stack nil) + +(defmacro org-ql--peg-parse-string (rules string &optional noerror) + "Parse STRING according to RULES." + ;; This sentence was in the docstring but Checkdoc is complaining, + ;; so moving it to a comment: "If NOERROR is non-nil, push nil + ;; resp. t if the parse failed resp. succeded instead of signaling + ;; an error." + + ;; Unfortunately, this macro was moved to peg-tests.el, so we copy it here. + `(with-temp-buffer + (insert ,string) + (goto-char (point-min)) + ,(if noerror + (let ((entry (make-symbol "entry")) + (start (caar rules))) + `(peg-parse (,entry (or (and ,start `(-- t)) "")) + . ,rules)) + `(peg-parse . ,rules)))) + +(cl-eval-when (compile load eval) + ;; This `eval-when' is necessary, otherwise the macro does not define + ;; the function correctly, apparently because `org-ql-predicates' + ;; ends up being not defined correctly at expansion time. + + (defun org-ql--def-query-string-to-sexp-fn (predicates) + "Define function `org-ql--query-string-to-sexp' according to PREDICATES. +Builds the PEG expression using PREDICATES (which should be the +value of `org-ql-predicates')." + (let* ((names (--map (symbol-name (plist-get (cdr it) :name)) + predicates)) + (aliases (->> predicates + (--map (plist-get (cdr it) :aliases)) + -non-nil + -flatten + (-map #'symbol-name))) + (predicates (->> (append names aliases) + -uniq + ;; Sort the keywords longest-first to work around what seems to be an + ;; obscure bug in `peg': when one keyword is a substring of another, + ;; and the shorter one is listed first, the shorter one fails to match. + (-sort (-on #'> #'length))))) + (fset 'org-ql--query-string-to-sexp + (byte-compile + `(cl-function + (lambda (input &optional (boolean 'and)) + "Return query parsed from plain query string INPUT. +Multiple predicates are combined with BOOLEAN." + (unless (s-blank-str? input) + (let* ((query (org-ql--peg-parse-string + ((query (+ term + (opt (+ (syntax-class whitespace) (any))))) + (term (or (and negation (list positive-term) + ;; This is a bit confusing, but it seems to work. There's probably a better way. + `(pred -- (list 'not (car pred)))) + positive-term)) + (positive-term (or (and predicate-with-args `(pred args -- (cons (intern pred) args))) + (and predicate-without-args `(pred -- (list (intern pred)))) + (and plain-string `(s -- (list 'regexp s))))) + (plain-string (or quoted-arg unquoted-arg)) + (predicate-with-args (substring predicate) ":" args) + (predicate-without-args (substring predicate) ":") + (predicate (or ,@predicates)) + (args (list (+ (and (or keyword-arg quoted-arg unquoted-arg) (opt separator))))) + (keyword-arg (and keyword "=" `(kw -- (intern (concat ":" kw))))) + (keyword (substring (+ (not (or separator "=" "\"" (syntax-class whitespace))) (any)))) + (quoted-arg "\"" (substring (+ (not (or separator "\"")) (any))) "\"") + (unquoted-arg (substring (+ (not (or separator "\"" (syntax-class whitespace))) (any)))) + (negation "!") + (separator "," )) + input 'noerror))) + ;; Discard the t that `peg-parse-string' always returns as the first + ;; element. I don't know what it means, but we don't want it. + (if (> (length (cdr query)) 1) + (cons boolean (nreverse (cdr query))) + (cadr query))))))))))) + +;;;;; Predicate definition + +(defvar org-ql-defpred-defer nil + "Defer expensive function redefinitions when defining predicates. +When non-nil, defining a predicate with `org-ql-defpred' does not +cause the functions `org-ql--normalize-query', +`org-ql--query-preamble', and `org-ql--query-string-to-sexp' to +be redefined. These functions must be redefined in order to +account for new predicates, but when defining many +predicates (like at load time), that may be deferred for +performance (after which those functions should be updated +manually; see the definition of `org-ql-defpred').") + +;; Yes, these two functions are a little hairy: `pcase' is challenging +;; enough, but splicing forms into one is something else. But it's +;; worth it to do this ugly stuff here, in one place, so the +;; `org-ql-defpred' macro becomes easy to use. + +(defun org-ql--define-normalize-query-fn (predicates) + "Define function `org-ql--normalize-query' for PREDICATES. +PREDICATES should be the value of `org-ql-predicates'." + (let ((normalizer-patterns (->> predicates + (--map (plist-get (cdr it) :normalizers)) + (-flatten-n 1)))) + (fset 'org-ql--normalize-query + (byte-compile + `(lambda (query) + "Return normalized form of QUERY expression. +This function is defined by calling +`org-ql--define-normalize-query-fn', which uses normalizer forms +defined in `org-ql-predicates' by calling `org-ql-defpred'." + (cl-labels ((rec (element) + (pcase element + (`(or . ,clauses) `(or ,@(mapcar #'rec clauses))) + (`(and . ,clauses) `(and ,@(mapcar #'rec clauses))) + (`(not . ,clauses) `(not ,@(mapcar #'rec clauses))) + (`(when ,condition . ,clauses) `(when ,(rec condition) + ,@(mapcar #'rec clauses))) + (`(unless ,condition . ,clauses) `(unless ,(rec condition) + ,@(mapcar #'rec clauses))) + ;; TODO: Combine (regexp) when appropriate (i.e. inside an OR, not an AND). + ((pred stringp) `(regexp ,element)) + + ,@normalizer-patterns + + ;; Any other form: passed through unchanged. + (_ element)))) + (rec query))))))) + +(defun org-ql--define-query-preamble-fn (predicates) + "Define function `org-ql--query-preamble' for PREDICATES. +PREDICATES should be the value of `org-ql-predicates'." + ;; NOTE: I don't how the `list' symbol ends up in the list, but anyway... + (let* ((preamble-patterns + (-flatten-n 1 (-non-nil + ;; NOTE: Using -let instead of pcase-let here because I can't make map 2.1 install in the test sandbox. + (--map (-let* (((&plist :preambles) (cdr it))) + (--map (pcase-let* ((`(,pattern ,exp) it)) + `(,pattern + (-let* (((&plist :regexp :case-fold :query) ,exp)) + (setf org-ql-preamble regexp + preamble-case-fold case-fold) + ;; NOTE: Even when `predicate' is nil, it must be returned in the pcase form. + query))) + preambles)) + predicates))))) + (fset 'org-ql--query-preamble + `(lambda (query) + "Return preamble data plist for QUERY. +The plist has the following keys: + + :preamble Regexp to search the Org buffer for to find potential + matches. If the regexp doesn't guarantee a match to + QUERY, the :query key should be an appropriate query + expression to test against the heading (i.e. usually + QUERY again). + + :preamble-case-fold What to bind variable `case-fold-search' to + around the regexp search. + + :query The query expression to use instead of QUERY (or QUERY + again, when appropriate). + +This function is defined by calling +`org-ql--define-query-preamble-fn', which uses preamble forms +defined in `org-ql-predicates' by calling `org-ql-defpred'." + (pcase org-ql-use-preamble + ('nil (list :query query :preamble nil)) + (_ (let ((preamble-case-fold t) + org-ql-preamble) + (cl-labels ((rec (element) + (or (when org-ql-preamble + ;; Only one preamble is allowed + element) + (pcase element + (`(or _) element) + + ,@preamble-patterns + + (`(and . ,rest) + (let ((clauses (mapcar #'rec rest))) + `(and ,@(-non-nil clauses)))) + (_ element))))) + (setq query (pcase (mapcar #'rec (list query)) + ((or `(nil) + `((nil)) + `((and)) + `((or))) + t) + (`(t) t) + (query (-flatten-n 1 query)))) + (list :query query :preamble org-ql-preamble :preamble-case-fold preamble-case-fold))))))) + ;; For some reason, byte-compiling the backquoted lambda form directly causes a warning + ;; that `query' refers to an unbound variable, even though that's not the case, and the + ;; function still works. But to avoid the warning, we byte-compile it afterward. + (byte-compile 'org-ql--query-preamble))) + +(cl-defmacro org-ql-defpred (name args docstring &key body preambles normalizers) + "Define an `org-ql' selector predicate named `org-ql--predicate-NAME'. +NAME may be a symbol or a list of symbols: if a list, the first +is used as NAME and the rest are aliases. A function is only +created for NAME, not for aliases, so a normalizer should be used +to replace aliases with NAME in queries (keep reading). + +ARGS is a `cl-defun'-style argument list. DOCSTRING is the +function's docstring. + +BODY is the body of the predicate. It will be evaluated with +point on the beginning of an Org heading and should return +non-nil if the heading's entry is a match. + +PREAMBLES and NORMALIZERS are lists of `pcase' forms matched +against Org QL query sexps. They are spliced into `pcase' forms +in the definitions of the functions `org-ql--query-preamble' and +`org-ql--normalize-query', which see. Those functions are +redefined when this macro is expanded, unless variable +`org-ql-defpred-defer' is non-nil, in which case those functions +should be redefined manually after defining predicates by calling +`org-ql--define-query-preamble-fn' and `org-ql--define-normalize-query-fn'. + +NORMALIZERS are used to normalize query expressions to standard +forms. For example, when the predicate has aliases, the aliases +should be replaced with predicate names using a normalizer. +Also, predicate arguments may be put into a more optimal form so +that the predicate has less work to do at query time. + +PREAMBLES refer to regular expressions which may be used to +search through a buffer directly to a potential match rather than +testing the predicate body on each heading. (Naming things is +hard.) In each `pcase' form in PREAMBLES, the `pcase' +expression (not the pattern) should be a plist with the following +keys, each value of which should be an expression which may refer +to variables bound in the pattern: + + :regexp Regular expression which searches directly to a + potential match. + + :case-fold Bound to `case-fold-search' around the regexp search. + + :query Expression which should replace the query expression, + or `query' if it should not be changed (e.g. if the + regexp is insufficient to determine whether a + heading matches, in which case the predicate's body + needs to be tested on the heading). If the regexp + guarantees a match, this may be simply t, leaving the + query expression with no work to do, which improves + performance. + +For convenience, within the `pcase' patterns, the symbol +`predicate-names' is a special form which is replaced with a +pattern matching any of the predicate's name and aliases. For +example, if NAME were: + + (heading h) + +Then if NORMALIZERS were: + + ((`(,predicate-names . ,args) + `(heading ,@args))) + +It would be expanded to: + + ((`(,(or 'heading 'h) . ,args) + `(heading ,@args)))" + ;; NOTE: The debug form works, completely! For example, use `edebug-defun' + ;; on the `heading' predicate, then evaluate this form: + ;; (let* ((query '(heading "HEADING")) + ;; (normalized (org-ql--normalize-query query)) + ;; (preamble (org-ql--query-preamble normalized))) + ;; (list :query query + ;; :normalized normalized + ;; :preamble preamble)) + (declare (debug ([&or symbolp listp] listp stringp + &rest [&or [":body" def-body] + [":normalizers" (&rest (sexp def-body))] + [":preambles" (&rest (sexp def-body))]])) + (indent defun)) + (let* ((aliases (when (listp name) + (cdr name))) + (name (cl-etypecase name + (list (car name)) + (atom name))) + (fn-name (intern (concat "org-ql--predicate-" (symbol-name name)))) + (predicate-name (intern (symbol-name name))) + (predicate-names (delq nil (cons predicate-name aliases))) + (normalizers (cl-sublis (list (cons 'predicate-names (cons 'or (--map (list 'quote it) predicate-names)))) + normalizers)) + (preambles (cl-sublis (list (cons 'predicate-names (cons 'or (--map (list 'quote it) predicate-names)))) + preambles))) + `(progn + (cl-eval-when (compile load eval) + (cl-defun ,fn-name ,args ,docstring ,body)) + (setf (map-elt org-ql-predicates ',predicate-name) + `(:name ,',name :aliases ,',aliases :fn ,',fn-name :docstring ,,docstring :args ,',args + :normalizers ,',normalizers :preambles ,',preambles)) + (unless org-ql-defpred-defer + ;; Reversing preserves the order in which predicates were defined. + (org-ql--define-normalize-query-fn (reverse org-ql-predicates)) + (org-ql--define-query-preamble-fn (reverse org-ql-predicates)) + (org-ql--def-query-string-to-sexp-fn (reverse org-ql-predicates)))))) + (defmacro org-ql--from-to-on () "For internal use. Expands into a form that processes arguments to timestamp-related @@ -1043,95 +1042,164 @@ predicates." ((pred stringp) (ts-parse-fill 'end to)) ((pred ts-p) to)))))) -(defun org-ql--byte-compile-warning (_string _pos _fill level) - "Signal an `org-ql-invalid-query' error. -Arguments STRING, POS, FILL, and LEVEL are according to -`byte-compile-log-warning-function'." - ;; Used as the `byte-compile-log-warning-function' in `org-ql--query-preamble'. - (signal 'org-ql-invalid-query level)) +;;;;;; Predicates -(defun org-ql--query-predicate (query) - "Return predicate function for QUERY." - ;; Use custom log function to prevent warnings for e.g. partially typed queries. - (let ((byte-compile-log-warning-function #'org-ql--byte-compile-warning)) - (byte-compile - `(lambda () - (cl-macrolet ((clocked (&key from to on) - (org-ql--from-to-on) - `(org-ql--predicate-clocked :from ,from :to ,to)) - (closed (&key from to on) - (org-ql--from-to-on) - `(org-ql--predicate-closed :from ,from :to ,to)) - (deadline (&key from to on) - (org-ql--from-to-on) - `(org-ql--predicate-deadline :from ,from :to ,to)) - (planning (&key from to on) - (org-ql--from-to-on) - `(org-ql--predicate-planning :from ,from :to ,to)) - (scheduled (&key from to on) - (org-ql--from-to-on) - `(org-ql--predicate-scheduled :from ,from :to ,to)) - (ts (&key from to on (type 'both)) - (org-ql--from-to-on) - `(org-ql--predicate-ts :from ,from :to ,to - :regexp ,(pcase type - ('both org-tsr-regexp-both) - ('active org-tsr-regexp) - ('inactive org-ql-tsr-regexp-inactive))))) - ,query))))) +(cl-eval-when (compile load eval) + ;; Improve load time by deferring the per-predicate preamble- and normalizer-function + ;; redefinitions until all of the predicates have been defined. + (setf org-ql-defpred-defer t)) -;;;;; Predicates - -(org-ql--defpred category (&rest categories) +(org-ql-defpred category (&rest categories) "Return non-nil if current heading is in one or more of CATEGORIES (a list of strings)." - (when-let ((category (org-get-category (point)))) - (cl-typecase categories - (null t) - (otherwise (member category categories))))) + :body (when-let ((category (org-get-category (point)))) + (cl-typecase categories + (null t) + (otherwise (member category categories))))) -(org-ql--defpred path (&rest regexps) - "Return non-nil if current heading's buffer's filename path matches any of REGEXPS (regexp strings). -Without arguments, return non-nil if buffer is file-backed." - (when (buffer-file-name) - (cl-typecase regexps - (null t) - (list (cl-loop for regexp in regexps - thereis (string-match regexp (buffer-file-name))))))) - -(org-ql--defpred todo (&rest keywords) - "Return non-nil if current heading is a TODO item. -With KEYWORDS, return non-nil if its keyword is one of KEYWORDS (a list of strings)." - (when-let ((state (org-get-todo-state))) - (cl-typecase keywords - (null (not (member state org-done-keywords))) - (list (member state keywords)) - (symbol (member state (symbol-value keywords))) - (otherwise (user-error "Invalid todo keywords: %s" keywords))))) - -(org-ql--defpred done () +(org-ql-defpred done () "Return non-nil if entry's TODO keyword is in `org-done-keywords'." ;; NOTE: This was a defsubst before being defined with the macro. Might be good to make it a defsubst again. - (or (apply #'org-ql--predicate-todo org-done-keywords))) + :body (or (apply #'org-ql--predicate-todo org-done-keywords))) -(org-ql--defpred (tags tags-all tags&) (&rest tags) - ;; NOTE: tags-all and tags& are "virtual" predicates that are handled by query pre-processing. - "Return non-nil if current heading has one or more of TAGS (a list of strings). -Tests both inherited and local tags." - (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)))))))) +(org-ql-defpred habit () + "Return non-nil if entry is a habit." + :preambles ((`(,predicate-names) + (list :regexp (rx bol (0+ space) ":STYLE:" (1+ space) "habit" (0+ space) eol)))) + :body (org-is-habit-p)) + +(org-ql-defpred (heading h) (&rest regexps) + "Return non-nil if current entry's heading matches all REGEXPS (regexp strings)." + :normalizers ((`(,predicate-names . ,args) + ;; "h" alias. + `(heading ,@args))) + ;; MAYBE: Adjust regexp to avoid matching in tag list. + :preambles ((`(,predicate-names ,regexp) + ;; Only one regexp: match with preamble, then let predicate confirm (because + ;; the match could be in e.g. the tags rather than the heading text). + (list :regexp (rx-to-string `(seq bol (1+ "*") (1+ blank) (0+ nonl) + ,regexp) + 'no-group) + :query query)) + (`(,predicate-names . ,regexps) + ;; Multiple regexps: use preamble to match against first + ;; regexp, then let the predicate match the rest. + (list :regexp (rx-to-string `(seq bol (1+ "*") (1+ blank) (0+ nonl) + ,(car regexps)) + 'no-group) + :query query))) + ;; TODO: In Org 9.2+, `org-get-heading' takes 2 more arguments. + :body (let ((heading (org-get-heading 'no-tags 'no-todo))) + (--all? (string-match it heading) regexps))) + +(org-ql-defpred level (level-or-comparator &optional level) + "Return non-nil if current heading's outline level matches arguments. +The following forms are accepted: + + (level NUMBER): Matches if heading level is NUMBER. + (level NUMBER NUMBER): Matches if heading level is equal to or between NUMBERs. + (level COMPARATOR NUMBER): Matches if heading level compares to NUMBER with COMPARATOR. + +COMPARATOR may be `<', `<=', `>', or `>='." + :normalizers ((`(,predicate-names . ,args) + ;; Arguments could be given as strings (e.g. from a non-Lisp query). + `(level ,@(--map (pcase it + ((or "<" "<=" ">" ">=" "=") + (intern it)) + ((pred stringp) (string-to-number it)) + (_ it)) + args)))) + :preambles ((`(,predicate-names ,comparator-or-num ,num) + (let ((repeat (pcase comparator-or-num + ('< `(repeat 1 ,(1- num) "*")) + ('<= `(repeat 1 ,num "*")) + ('> `(>= ,(1+ num) "*")) + ('>= `(>= ,num "*")) + ((pred integerp) `(repeat ,comparator-or-num ,num "*"))))) + (list :regexp (rx-to-string `(seq bol ,repeat " ") t) + :case-fold t))) + (`(,predicate-names ,num) + (list :regexp (rx-to-string `(seq bol (repeat ,num "*") " ") t) + :case-fold t))) + ;; NOTE: It might be necessary to take into account `org-odd-levels'; see docstring for + ;; `org-outline-level'. + :body (when-let ((outline-level (org-outline-level))) + (pcase level-or-comparator + ((pred numberp) (pcase level + ('nil ;; Equality + (= outline-level level-or-comparator)) + ((pred numberp) ;; Between two levels + (>= level-or-comparator outline-level level)))) + ((pred symbolp) ;; Compare with function + (funcall level-or-comparator outline-level level))))) + +(org-ql-defpred link (&rest args) + ;; User-facing argument form: (&optional description-or-target &key description target regexp-p). + "Return non-nil if current heading contains a link matching arguments. +DESCRIPTION-OR-TARGET is matched against the link's description +and target. Alternatively, one or both of DESCRIPTION and TARGET +may be matched separately. Without arguments, return non-nil if +any link is found." + ;; NOTE: It would be preferable to avoid this manual argument parsing every time the predicate + ;; is called, but pre-processing it to a normal form gets complicated with the preamble and + ;; pre-processing, because we don't want to display a query like "(link :description-or-target + ;; "FOO")" in the view header, which would be ugly. So, since preambles are expected to be + ;; enabled nearly all of the time, in which case this function won't be called anyway, it's + ;; probably not worth rewriting code all over the place to fix this. + :preambles ((`(,predicate-names) + (list :regexp ;; Match a link with a target and optionally a description. + (rx (or bol (1+ blank)) + "[[" (1+ (not (any "]"))) "]" + (optional (seq "[" (0+ (not (any "]"))) "]")) + "]" + (or eol blank)))) + (`(,predicate-names ,(and description-or-target + (guard (not (keywordp description-or-target))))) + (list :regexp (org-ql--link-regexp :description-or-target + (regexp-quote description-or-target))) + nil) + (`(,predicate-names . ,plist) + (list :regexp (org-ql--link-regexp + :description + (when (plist-get plist :description) + (regexp-quote (plist-get plist :description))) + :target (when (plist-get plist :target) + (regexp-quote (plist-get plist :target))))) + nil)) + :body (let* (plist description-or-target description target regexp-p) + (if (not (keywordp (car args))) + (setf description-or-target (car args) + plist (cdr args)) + (setf plist args)) + (setf description (plist-get plist :description) + target (plist-get plist :description) + regexp-p (plist-get plist :regexp-p)) + (unless regexp-p + ;; NOTE: It would also be preferable to avoid regexp-quoting every time this predicate + ;; is called. Ideally that would be handled in the query pre-processing step. However, + ;; handling that properly, in combination with preparing the query preamble and whether + ;; REGEXP-P is enabled, is also complicated, so let's not. + (when description-or-target + (setf description-or-target (regexp-quote description-or-target))) + (when description + (setf description (regexp-quote description))) + (when target + (setf target (regexp-quote target)))) + (when (re-search-forward org-ql-link-regexp (org-entry-end-position) t) + (pcase description-or-target + ('nil (and (or (null target) + (string-match-p target (match-string 1))) + (or (null description) + (string-match-p description (match-string org-ql-link-description-group))))) + (_ (if (and description target) + (and (string-match-p target (match-string 1)) + (string-match-p description (match-string org-ql-link-description-group))) + (or (string-match-p description-or-target (match-string 1)) + (string-match-p description-or-target + (match-string org-ql-link-description-group))))))))) ;; MAYBE: Preambles for outline-path predicates. Not sure if possible without complicated logic. -(org-ql--defpred (outline-path olp) (&rest regexps) +(org-ql-defpred (outline-path olp) (&rest regexps) "Return non-nil if current node's outline path matches all of REGEXPS. Each string is compared as a regexp to each element of the node's outline path with `string-match'. For example, if an entry's @@ -1143,11 +1211,14 @@ the following queries: (olp \"Food\" \"Fruits\") (olp \"Fruits\" \"Grapes\") (olp \"Food\" \"Grapes\")" - (let ((entry-olp (org-ql--value-at (point) #'org-ql--outline-path))) - (cl-loop for h in regexps - always (cl-member h entry-olp :test #'string-match)))) + :normalizers ((`(,predicate-names . ,strings) + ;; Regexp quote headings. + `(outline-path ,@(mapcar #'regexp-quote strings)))) + :body (let ((entry-olp (org-ql--value-at (point) #'org-ql--outline-path))) + (cl-loop for h in regexps + always (cl-member h entry-olp :test #'string-match)))) -(org-ql--defpred (outline-path-segment olps) (&rest regexps) +(org-ql-defpred (outline-path-segment olps) (&rest regexps) "Return non-nil if current node's outline path matches segment REGEXPS. Matches REGEXPS as a contiguous segment of the outline path. Each regexp is compared to each element of the node's outline @@ -1165,118 +1236,21 @@ contiguous segment of the outline path: (olp \"Food\" \"Grape\")" ;; MAYBE: Allow anchored matching. - (org-ql--infix-p regexps (org-ql--value-at (point) #'org-ql--outline-path))) + :normalizers ((`(,(or 'outline-path-segment 'olps) . ,strings) + ;; Regexp quote headings. + `(outline-path-segment ,@(mapcar #'regexp-quote strings)))) + :body (org-ql--infix-p regexps (org-ql--value-at (point) #'org-ql--outline-path))) -(org-ql--defpred (tags-inherited tags-i itags) (&rest tags) - "Return non-nil if current heading's inherited tags include one or more of TAGS (a list of strings). -If TAGS is nil, return non-nil if heading has any inherited tags." - (cl-macrolet ((tags-p (tags) - `(and ,tags - (not (eq 'org-ql-nil ,tags))))) - (-let* (((inherited _) (org-ql--tags-at (point)))) - (cl-typecase tags - (null (tags-p inherited)) - (otherwise (when (tags-p inherited) - (seq-intersection tags inherited))))))) +(org-ql-defpred path (&rest regexps) + "Return non-nil if current heading's buffer's filename path matches any of REGEXPS (regexp strings). +Without arguments, return non-nil if buffer is file-backed." + :body (when (buffer-file-name) + (cl-typecase regexps + (null t) + (list (cl-loop for regexp in regexps + thereis (string-match regexp (buffer-file-name))))))) -(org-ql--defpred (tags-local tags-l ltags) (&rest tags) - "Return non-nil if current heading's local tags include one or more of TAGS (a list of strings). -If TAGS is nil, return non-nil if heading has any local tags." - (cl-macrolet ((tags-p (tags) - `(and ,tags - (not (eq 'org-ql-nil ,tags))))) - (-let* (((_ local) (org-ql--tags-at (point)))) - (cl-typecase tags - (null (tags-p local)) - (otherwise (when (tags-p local) - (seq-intersection tags local))))))) - -(org-ql--defpred (tags-regexp tags*) (&rest regexps) - "Return non-nil if current heading has tags matching one or more of REGEXPS. -Tests both inherited and local tags." - (cl-macrolet ((tags-p (tags) - `(and ,tags - (not (eq 'org-ql-nil ,tags))))) - (-let* (((inherited local) (org-ql--tags-at (point)))) - (cl-typecase regexps - (null (or (tags-p inherited) - (tags-p local))) - (otherwise (or (when (tags-p inherited) - (cl-loop for tag in inherited - thereis (cl-loop for regexp in regexps - thereis (string-match regexp tag)))) - (when (tags-p local) - (cl-loop for tag in local - thereis (cl-loop for regexp in regexps - thereis (string-match regexp tag)))))))))) - -(org-ql--defpred level (level-or-comparator &optional level) - "Return non-nil if current heading's outline level matches arguments. -The following forms are accepted: - - (level NUMBER): Matches if heading level is NUMBER. - (level NUMBER NUMBER): Matches if heading level is equal to or between NUMBERs. - (level COMPARATOR NUMBER): Matches if heading level compares to NUMBER with COMPARATOR. - -COMPARATOR may be `<', `<=', `>', or `>='." - ;; NOTE: It might be necessary to take into account `org-odd-levels'; see docstring for - ;; `org-outline-level'. - (when-let ((outline-level (org-outline-level))) - (pcase level-or-comparator - ((pred numberp) (pcase level - ('nil ;; Equality - (= outline-level level-or-comparator)) - ((pred numberp) ;; Between two levels - (>= level-or-comparator outline-level level)))) - ((pred symbolp) ;; Compare with function - (funcall level-or-comparator outline-level level))))) - -(org-ql--defpred link (&rest args) - ;; User-facing argument form: (&optional description-or-target &key description target regexp-p). - "Return non-nil if current heading contains a link matching arguments. -DESCRIPTION-OR-TARGET is matched against the link's description -and target. Alternatively, one or both of DESCRIPTION and TARGET -may be matched separately. Without arguments, return non-nil if -any link is found." - ;; NOTE: It would be preferable to avoid this manual argument parsing every time the predicate - ;; is called, but pre-processing it to a normal form gets complicated with the preamble and - ;; pre-processing, because we don't want to display a query like "(link :description-or-target - ;; "FOO")" in the view header, which would be ugly. So, since preambles are expected to be - ;; enabled nearly all of the time, in which case this function won't be called anyway, it's - ;; probably not worth rewriting code all over the place to fix this. - (let* (plist description-or-target description target regexp-p) - (if (not (keywordp (car args))) - (setf description-or-target (car args) - plist (cdr args)) - (setf plist args)) - (setf description (plist-get plist :description) - target (plist-get plist :description) - regexp-p (plist-get plist :regexp-p)) - (unless regexp-p - ;; NOTE: It would also be preferable to avoid regexp-quoting every time this predicate - ;; is called. Ideally that would be handled in the query pre-processing step. However, - ;; handling that properly, in combination with preparing the query preamble and whether - ;; REGEXP-P is enabled, is also complicated, so let's not. - (when description-or-target - (setf description-or-target (regexp-quote description-or-target))) - (when description - (setf description (regexp-quote description))) - (when target - (setf target (regexp-quote target)))) - (when (re-search-forward org-ql-link-regexp (org-entry-end-position) t) - (pcase description-or-target - ('nil (and (or (null target) - (string-match-p target (match-string 1))) - (or (null description) - (string-match-p description (match-string org-ql-link-description-group))))) - (_ (if (and description target) - (and (string-match-p target (match-string 1)) - (string-match-p description (match-string org-ql-link-description-group))) - (or (string-match-p description-or-target (match-string 1)) - (string-match-p description-or-target - (match-string org-ql-link-description-group))))))))) - -(org-ql--defpred priority (&rest args) +(org-ql-defpred priority (&rest args) "Return non-nil if current heading has a certain priority. ARGS may be either a list of one or more priority letters as strings, or a comparator function symbol followed by a priority @@ -1296,6 +1270,42 @@ priority B)." ;; value. We do this because it doesn't seem very useful or intuitive for a ;; query like (priority "B") to match an item that has no priority cookie. ;; TODO: Convert priority arg(s) to numeric values in pre-processing. + :normalizers + ((`(,predicate-names ,(and (or '= '< '> '<= '>=) comparator) ,letter) + ;; Quote comparator. + `(priority ',comparator ,letter))) + + :preambles + (;; NOTE: This only accepts A, B, or C. I haven't seen + ;; other priorities in the wild, so this will do for now. + (`(,predicate-names) + ;; Any priority cookie. + (list :regexp (rx-to-string `(seq bol (1+ "*") (1+ blank) (0+ nonl) "[#" (in "ABC") "]") t))) + (`(,predicate-names ,(and (or ''= ''< ''> ''<= ''>=) comparator) ,letter) + ;; Comparator and priority letter. + ;; NOTE: The double-quoted comparators. See below. + (let* ((priority-letters '("A" "B" "C")) + (index (-elem-index letter priority-letters)) + ;; NOTE: Higher priority == lower number. + ;; NOTE: Because we need to support both preamble-based queries and + ;; regular predicate ones, we work around an idiosyncrasy of query + ;; pre-processing by accepting both quoted and double-quoted comparator + ;; function symbols. Not the most elegant solution, but it works. + (priorities (s-join "" (pcase comparator + ((or '= ''=) (list letter)) + ((or '> ''>) (cl-subseq priority-letters 0 index)) + ((or '>= ''>=) (cl-subseq priority-letters 0 (1+ index))) + ((or '< ''<) (cl-subseq priority-letters (1+ index))) + ((or '<= ''<=) (cl-subseq priority-letters index)))))) + (list :regexp (rx-to-string `(seq bol (1+ "*") (1+ blank) (optional (1+ upper) (1+ blank)) + "[#" (in ,priorities) "]") t)))) + (`(,predicate-names . ,letters) + ;; One or more priorities. + ;; MAYBE: Disable case-folding. + (list :regexp (rx-to-string `(seq bol (1+ "*") (1+ blank) + (optional (1+ upper) (1+ blank)) + "[#" (or ,@letters) "]") t)))) + :body (when-let* ((item-priority (save-excursion (save-match-data ;; TODO: Is the save-match-data above necessary? @@ -1317,29 +1327,32 @@ priority B)." (cl-loop for priority-arg in args thereis (= item-priority (* 1000 (- org-lowest-priority (string-to-char priority-arg))))))))) -(org-ql--defpred habit () - "Return non-nil if entry is a habit." - (org-is-habit-p)) - -(org-ql--defpred (regexp r) (&rest regexps) - "Return non-nil if current entry matches all of REGEXPS (regexp strings)." - (let ((end (or (save-excursion - (outline-next-heading)) - (point-max)))) - (save-excursion - (goto-char (line-beginning-position)) - (cl-loop for regexp in regexps - always (save-excursion - (re-search-forward regexp end t)))))) - -(org-ql--defpred (heading h) (&rest regexps) - "Return non-nil if current entry's heading matches all REGEXPS (regexp strings)." - ;; TODO: In Org 9.2+, `org-get-heading' takes 2 more arguments. - (let ((heading (org-get-heading 'no-tags 'no-todo))) - (--all? (string-match it heading) regexps))) - -(org-ql--defpred property (property &optional value) +(org-ql-defpred property (property &optional value) "Return non-nil if current entry has PROPERTY (a string), and optionally VALUE (a string)." + :normalizers ((`(,predicate-names ,property . ,value) + ;; Convert keyword property arguments to strings. Non-sexp + ;; queries result in keyword property arguments (because to do + ;; otherwise would require ugly special-casing in the parsing). + (when (keywordp property) + (setf property (substring (symbol-name property) 1))) + (cons 'property (cons property value)))) + ;; MAYBE: Should case folding be disabled for properties? What about values? + ;; MAYBE: Support (property) without args. + :preambles ((`(,predicate-names ,property ,value) + ;; We do NOT return nil, because the predicate still needs to be tested, + ;; because the regexp could match a string not inside a property drawer. + (list :regexp (rx-to-string `(seq bol (0+ space) ":" ,property ":" + (1+ space) ,value (0+ space) eol)) + :query query)) + (`(,predicate-names ,property) + ;; We do NOT return nil, because the predicate still needs to be tested, + ;; because the regexp could match a string not inside a property drawer. + ;; NOTE: The preamble only matches if there appears to be a value. + ;; A line like ":ID: " without any other text does not match. + (list :regexp (rx-to-string `(seq bol (0+ space) ":" ,property ":" (1+ space) + (minimal-match (1+ not-newline)) eol)) + :query query))) + :body (pcase property ('nil (user-error "Property matcher requires a PROPERTY argument")) (_ (pcase value @@ -1350,10 +1363,53 @@ priority B)." ;; Check that PROPERTY has VALUE (string-equal value (org-entry-get (point) property 'selective))))))) -(org-ql--defpred src (&key regexps lang) +(org-ql-defpred (regexp r) (&rest regexps) + "Return non-nil if current entry matches all of REGEXPS (regexp strings)." + :normalizers ((`(,predicate-names . ,args) + `(regexp ,@args))) + ;; MAYBE: Separate case-sensitive (Regexp) predicate. + :preambles ((`(,predicate-names ,regexp) + (list :case-fold t :regexp regexp :query t)) + (`(,predicate-names . ,regexps) + ;; Search for first regexp, then confirm with predicate. + (list :case-fold t :regexp (car regexps) :query query))) + :body + (let ((end (or (save-excursion + (outline-next-heading)) + (point-max)))) + (save-excursion + (goto-char (line-beginning-position)) + (cl-loop for regexp in regexps + always (save-excursion + (re-search-forward regexp end t)))))) + +(org-ql-defpred src (&key regexps lang) "Return non-nil if current entry contains an Org source block matching all of REGEXPS. If keyword argument LANG is non-nil, the block must be in that language." + :normalizers ((`(,predicate-names . ,args) + ;; Rewrite to use keyword args. + (-let (regexps lang keyword-index) + (cond ((plist-get args :lang) + ;; Lang given first, or only lang given. + (setf lang (plist-get args :lang) + regexps (seq-difference args (list :lang lang)))) + ((setf keyword-index (-find-index #'keywordp args)) + ;; Regexps and lang given. + (setf lang (plist-get (cl-subseq args keyword-index) :lang) + regexps (cl-subseq args 0 keyword-index))) + (t ;; Only regexps given. + (setf regexps args))) + (when regexps + ;; This feels awkward and wrong, but we have to quote lists + ;; and avoid quoting nil. There must be a better way. + (setf regexps `(',regexps))) + `(src :lang ,lang :regexps ,@regexps)))) + :preambles ((`(,predicate-names . ,args) + (list :regexp (org-ql--format-src-block-regexp (plist-get args :lang)) + ;; Always check contents with predicate. + :query query))) + :body (catch 'return (save-excursion (save-match-data @@ -1375,6 +1431,101 @@ language." ;; No regexps to check: return non-nil. t)))))) +(org-ql-defpred (tags) (&rest tags) + "Return non-nil if current heading has one or more of TAGS (a list of strings). +Tests both inherited and local tags." + ;; MAYBE: -all versions for inherited and local. + :body (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)))))))) + +(org-ql-defpred (tags-all tags&) (&rest tags) + "Return non-nil if current heading has all of TAGS (a list of strings). +Tests both inherited and local tags." + ;; MAYBE: -all versions for inherited and local. + :normalizers ((`(,predicate-names) `(tags)) + (`(,predicate-names . ,tags) `(and ,@(--map `(tags ,it) tags)))) + :body (apply #'org-ql--predicate-tags tags)) + +(org-ql-defpred (tags-inherited inherited-tags tags-i itags) (&rest tags) + "Return non-nil if current heading's inherited tags include one or more of TAGS (a list of strings). +If TAGS is nil, return non-nil if heading has any inherited tags." + :normalizers ((`(,predicate-names . ,tags) + `(tags-inherited ,@tags)) + (`(,predicate-names) + `(tags-inherited))) + :body (cl-macrolet ((tags-p (tags) + `(and ,tags + (not (eq 'org-ql-nil ,tags))))) + (-let* (((inherited _) (org-ql--tags-at (point)))) + (cl-typecase tags + (null (tags-p inherited)) + (otherwise (when (tags-p inherited) + (seq-intersection tags inherited))))))) + +(org-ql-defpred (tags-local local-tags tags-l ltags) (&rest tags) + "Return non-nil if current heading's local tags include one or more of TAGS (a list of strings). +If TAGS is nil, return non-nil if heading has any local tags." + :normalizers ((`(,predicate-names) `(tags-local)) + (`(,predicate-names . ,tags) `(tags-local ,@tags))) + :preambles ((`(,predicate-names . ,(and tags (guard tags))) + ;; When searching for local, non-inherited tags, we can + ;; search directly to headings containing one of the tags. + (list :regexp (rx-to-string `(seq bol (1+ "*") (1+ space) (1+ not-newline) + ":" (or ,@tags) ":") + t) + :query t))) + :body (cl-macrolet ((tags-p (tags) + `(and ,tags + (not (eq 'org-ql-nil ,tags))))) + (-let* (((_ local) (org-ql--tags-at (point)))) + (cl-typecase tags + (null (tags-p local)) + (otherwise (when (tags-p local) + (seq-intersection tags local))))))) + +(org-ql-defpred (tags-regexp tags*) (&rest regexps) + "Return non-nil if current heading has tags matching one or more of REGEXPS. +Tests both inherited and local tags." + :normalizers ((`(,predicate-names . ,regexps) + `(tags-regexp ,@regexps))) + :body (cl-macrolet ((tags-p (tags) + `(and ,tags + (not (eq 'org-ql-nil ,tags))))) + (-let* (((inherited local) (org-ql--tags-at (point)))) + (cl-typecase regexps + (null (or (tags-p inherited) + (tags-p local))) + (otherwise (or (when (tags-p inherited) + (cl-loop for tag in inherited + thereis (cl-loop for regexp in regexps + thereis (string-match regexp tag)))) + (when (tags-p local) + (cl-loop for tag in local + thereis (cl-loop for regexp in regexps + thereis (string-match regexp tag)))))))))) + +(org-ql-defpred todo (&rest keywords) + "Return non-nil if current heading is a TODO item. +With KEYWORDS, return non-nil if its keyword is one of KEYWORDS (a list of strings)." + ;; TODO: Can we make a preamble for plain (todo) queries? + :preambles ((`(,predicate-names . ,(and todo-keywords (guard todo-keywords))) + (list :case-fold nil :regexp (rx-to-string `(seq bol (1+ "*") (1+ space) (or ,@todo-keywords) (or " " eol)) t)))) + :body (when-let ((state (org-get-todo-state))) + (cl-typecase keywords + (null (not (member state org-done-keywords))) + (list (member state keywords)) + (symbol (member state (symbol-value keywords))) + (otherwise (user-error "Invalid todo keywords: %s" keywords))))) + ;;;;;; Ancestor/descendant ;; These predicates search ancestor and descendant headings for sub-queries. @@ -1393,14 +1544,20 @@ language." ;; rather than user-facing, since their arguments are predicates provided ;; automatically by `--pre-process-query'. -(org-ql--defpred ancestors (predicate) +(org-ql-defpred ancestors (predicate) "Return non-nil if any of current entry's ancestors satisfy PREDICATE." + :normalizers ((`(,predicate-names ,query) `(ancestors ,(org-ql--query-predicate (rec query)))) + (`(,predicate-names) '(ancestors (lambda () t)))) + :body (org-with-wide-buffer (cl-loop while (org-up-heading-safe) thereis (org-ql--value-at (point) predicate)))) -(org-ql--defpred parent (predicate) +(org-ql-defpred parent (predicate) "Return non-nil if the current entry's parent satisfies PREDICATE." + :normalizers ((`(,predicate-names ,query) `(parent ,(org-ql--query-predicate (rec query)))) + (`(,predicate-names) '(parent (lambda () t)))) + :body (org-with-wide-buffer (when (org-up-heading-safe) (org-ql--value-at (point) predicate)))) @@ -1411,8 +1568,12 @@ language." ;; on the Org file being searched and the sub-query, performance could be better or ;; worse. It should be benchmarked extensively before so changing the implementation. -(org-ql--defpred children (query) +(org-ql-defpred children (query) "Return non-nil if current entry has children matching QUERY." + ;; Quote children queries so the user doesn't have to. + :normalizers ((`(,predicate-names ,query) `(children ',query)) + (`(,predicate-names) '(children (lambda () t)))) + :body (org-with-wide-buffer ;; Widening is needed if inside an "ancestors" query (org-narrow-to-subtree) @@ -1432,10 +1593,13 @@ language." :action (lambda () (throw 'found t)))))))) -(org-ql--defpred descendants (query) +(org-ql-defpred descendants (query) "Return non-nil if current entry has descendants matching QUERY." ;; TODO: This could probably be rewritten like the `ancestors' predicate, ;; which avoids calling `org-ql-select' recursively and its associated overhead. + :normalizers ((`(,predicate-names ,query) `(descendants ',query)) + (`(,predicate-names) '(descendants (lambda () t)))) + :body (org-with-wide-buffer (org-narrow-to-subtree) (when (org-goto-first-child) @@ -1454,13 +1618,13 @@ language." ;; NOTE: These docstrings apply to the functions defined by `org-ql--defpref', ;; not necessarily to the way users are expected to call them in queries. The -;; queries are pre-processed by `org-ql--pre-process-query' to handle +;; queries are pre-processed by `org-ql--normalize-query' to handle ;; arguments which are constant during a query's execution. ;; TODO: Update the macro to define a user-facing docstring so I don't ;; have to manually update the documentation. -(org-ql--defpred clocked (&key from to _on) +(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. @@ -1477,9 +1641,20 @@ 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)) -(org-ql--defpred closed (&key from to _on) +(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. @@ -1496,10 +1671,20 @@ 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))) -(org-ql--defpred deadline (&key from to _on) +(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. @@ -1516,11 +1701,29 @@ 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))) -(org-ql--defpred deadline-warning (&key from to) +(org-ql-defpred deadline-warning (&key from to) "Internal selector used to handle `org-deadline-warning-days' and deadlines with warning periods." + :preambles ((`(,predicate-names . ,_) + (list :regexp org-deadline-time-regexp :query query))) + :body (save-excursion (forward-line 1) (when (re-search-forward org-deadline-time-regexp (line-end-position) t) @@ -1535,7 +1738,7 @@ parseable by `parse-time-string' which may omit the time value." ((_timestamp (&keys :warning-value :warning-unit)) deadline-ts-element) (ts (ts-parse-org-element deadline-ts-element))) (pcase warning-unit - ('nil ;; Deadline has no warning unit: compare with ts passed in. + ('nil ;; Deadline has no warning unit: compare with ts passed in. (cond ((and from to) (ts-in from to ts)) (from (ts<= from ts)) (to (ts<= ts to)))) @@ -1544,7 +1747,7 @@ parseable by `parse-time-string' which may omit the time value." (ts<= (->> ts (ts-adjust unit (- warning-value))) org-ql--today)) ('week (ts<= (->> ts (ts-adjust 'day (* -7 warning-value))) org-ql--today))))))) -(org-ql--defpred planning (&key from to _on) +(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. @@ -1561,10 +1764,18 @@ 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))) -(org-ql--defpred scheduled (&key from to _on) +(org-ql-defpred scheduled (&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. @@ -1581,12 +1792,20 @@ 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)))) + `(scheduled :to ,to)))) + :preambles ((`(,predicate-names . ,_) + (list :regexp org-scheduled-time-regexp :query query))) + :body (org-ql--predicate-ts :from from :to to :regexp org-scheduled-time-regexp :match-group 1 :limit (line-end-position 2))) -(org-ql--defpred (ts ts-active ts-a ts-inactive ts-i) +(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--pre-process-query'. + ;; 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 @@ -1613,7 +1832,23 @@ 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)))) @@ -1627,6 +1862,14 @@ of the line after the heading." (from (test-timestamps (ts<= from next-ts))) (to (test-timestamps (ts<= next-ts to))))))) +;; Predicates defined: stop deferring and define normalizer and preamble functions now. +(setf org-ql-defpred-defer nil) +;; Reversing preserves the order in which they were defined. +;; Generally it shouldn't matter, but it might... +(org-ql--define-normalize-query-fn (reverse org-ql-predicates)) +(org-ql--define-query-preamble-fn (reverse org-ql-predicates)) +(org-ql--def-query-string-to-sexp-fn (reverse org-ql-predicates)) + ;;;;; Sorting ;; TODO: These appear to work properly, but it would be good to have tests for them. @@ -1720,94 +1963,6 @@ element should be a regexp string." always (string-match i l)) do (pop list))) -;;;;; Plain query parsing - -;; This section implements parsing of "plain," non-Lisp queries using the `peg' -;; library. NOTE: This needs to appear after the predicates are defined. - -;; TODO: Rename "plain" to "string", or something like that. - -(require 'peg) - -;; Fix compiler warnings probably caused by `peg' not using lexical-binding. -;; TODO: File bug report upstream. -(defvar peg-errors) -(defvar peg-stack) - -(defmacro org-ql--peg-parse-string (rules string &optional noerror) - "Parse STRING according to RULES." - ;; This sentence was in the docstring but Checkdoc is complaining, - ;; so moving it to a comment: "If NOERROR is non-nil, push nil - ;; resp. t if the parse failed resp. succeded instead of signaling - ;; an error." - - ;; Unfortunately, this macro was moved to peg-tests.el, so we copy it here. - `(with-temp-buffer - (insert ,string) - (goto-char (point-min)) - ,(if noerror - (let ((entry (make-symbol "entry")) - (start (caar rules))) - `(peg-parse (,entry (or (and ,start `(-- t)) "")) - . ,rules)) - `(peg-parse . ,rules)))) - -(cl-eval-when (compile load eval) - ;; This `eval-when' is necessary, otherwise the macro does not define - ;; the function correctly, apparently because `org-ql-predicates' - ;; ends up being not defined correctly at expansion time. - - (defmacro org-ql--def-plain-query-fn () - "Define function `org-ql--plain-query'. -Builds the PEG expression using predicates defined in -`org-ql-predicates' and `org-ql-predicates-extra-aliases'." - (let* ((predicates (--map (symbol-name (plist-get it :name)) - org-ql-predicates)) - (aliases (->> org-ql-predicates - (--map (plist-get it :aliases)) - -non-nil - -flatten - (-map #'symbol-name))) - (predicates (->> (append predicates aliases) - -uniq - ;; Sort the keywords longest-first to work around what seems to be an - ;; obscure bug in `peg': when one keyword is a substring of another, - ;; and the shorter one is listed first, the shorter one fails to match. - (-sort (-on #'> #'length))))) - `(cl-defun org-ql--plain-query (input &optional (boolean 'and)) - "Return query parsed from plain query string INPUT. -Multiple predicates are combined with BOOLEAN." - (unless (s-blank-str? input) - (let* ((query (org-ql--peg-parse-string - ((query (+ term - (opt (+ (syntax-class whitespace) (any))))) - (term (or (and negation (list positive-term) - ;; This is a bit confusing, but it seems to work. There's probably a better way. - `(pred -- (list 'not (car pred)))) - positive-term)) - (positive-term (or (and predicate-with-args `(pred args -- (cons (intern pred) args))) - (and predicate-without-args `(pred -- (list (intern pred)))) - (and plain-string `(s -- (list 'regexp s))))) - (plain-string (or quoted-arg unquoted-arg)) - (predicate-with-args (substring predicate) ":" args) - (predicate-without-args (substring predicate) ":") - (predicate (or ,@predicates)) - (args (list (+ (and (or keyword-arg quoted-arg unquoted-arg) (opt separator))))) - (keyword-arg (and keyword "=" `(kw -- (intern (concat ":" kw))))) - (keyword (substring (+ (not (or separator "=" "\"" (syntax-class whitespace))) (any)))) - (quoted-arg "\"" (substring (+ (not (or separator "\"")) (any))) "\"") - (unquoted-arg (substring (+ (not (or separator "\"" (syntax-class whitespace))) (any)))) - (negation "!") - (separator "," )) - input 'noerror))) - ;; Discard the t that `peg-parse-string' always returns as the first - ;; element. I don't know what it means, but we don't want it. - (if (> (length (cdr query)) 1) - (cons boolean (nreverse (cdr query))) - (cadr query))))))) - - (org-ql--def-plain-query-fn)) - ;; And now we go the other direction... (defun org-ql--query-sexp-to-string (query) diff --git a/org-ql.info b/org-ql.info index abfce7a..1d693c3 100644 --- a/org-ql.info +++ b/org-ql.info @@ -63,6 +63,7 @@ Functions / Macros * Agenda-like views:: * Listing / acting-on results:: +* Custom predicates:: Changelog @@ -565,11 +566,13 @@ File: README.info, Node: Functions / Macros, Next: Dynamic block, Prev: Queri • • • - • • • + • • • • • + • * Menu: * Agenda-like views:: * Listing / acting-on results:: +* Custom predicates::  File: README.info, Node: Agenda-like views, Next: Listing / acting-on results, Up: Functions / Macros @@ -606,7 +609,7 @@ File: README.info, Node: Agenda-like views, Next: Listing / acting-on results, the block header, otherwise the header is formed automatically.  -File: README.info, Node: Listing / acting-on results, Prev: Agenda-like views, Up: Functions / Macros +File: README.info, Node: Listing / acting-on results, Next: Custom predicates, Prev: Agenda-like views, Up: Functions / Macros 4.3.2 Listing / acting-on results --------------------------------- @@ -737,6 +740,84 @@ File: README.info, Node: Listing / acting-on results, Prev: Agenda-like views, Expands into a call to ‘org-ql-select’ with the same arguments. For convenience, arguments should be unquoted. + +File: README.info, Node: Custom predicates, Prev: Listing / acting-on results, Up: Functions / Macros + +4.3.3 Custom predicates +----------------------- + + • See: Custom predicate tutorial (examples/defpred.org) + + 1. Macro: org-ql-defpred + + _Arguments:_ ‘(name args docstring &key body preambles + normalizers)’ + + Define an ‘org-ql’ selector predicate named + ‘org-ql--predicate-NAME’. ‘NAME’ may be a symbol or a list of + symbols: if a list, the first is used as ‘NAME’ and the rest are + aliases. ‘A’ function is only created for ‘NAME’, not for aliases, + so a normalizer should be used to replace aliases with ‘NAME’ in + queries (keep reading). + + ‘ARGS’ is a ‘cl-defun’-style argument list. ‘DOCSTRING’ is the + function’s docstring. + + ‘BODY’ is the body of the predicate. It will be evaluated with + point on the beginning of an Org heading and should return non-nil + if the heading’s entry is a match. + + ‘PREAMBLES’ and ‘NORMALIZERS’ are lists of ‘pcase’ forms matched + against Org ‘QL’ query sexps. They are spliced into ‘pcase’ forms + in the definitions of the functions ‘org-ql--query-preamble’ and + ‘org-ql--normalize-query’, which see. Those functions are + redefined when this macro is expanded, unless variable + ‘org-ql-defpred-defer’ is non-nil, in which case those functions + should be redefined manually after defining predicates by calling + ‘org-ql--define-query-preamble-fn’ and + ‘org-ql--define-normalize-query-fn’. + + ‘NORMALIZERS’ are used to normalize query expressions to standard + forms. For example, when the predicate has aliases, the aliases + should be replaced with predicate names using a normalizer. Also, + predicate arguments may be put into a more optimal form so that the + predicate has less work to do at query time. + + ‘PREAMBLES’ refer to regular expressions which may be used to + search through a buffer directly to a potential match rather than + testing the predicate body on each heading. (Naming things is + hard.) In each ‘pcase’ form in ‘PREAMBLES’, the ‘pcase’ expression + (not the pattern) should be a plist with the following keys, each + value of which should be an expression which may refer to variables + bound in the pattern: + + ‘:regexp’ Regular expression which searches directly to a potential + match. + + ‘:case-fold’ Bound to ‘case-fold-search’ around the regexp search. + + ‘:query’ Expression which should replace the query expression, or + ‘query’ if it should not be changed (e.g. if the regexp is + insufficient to determine whether a heading matches, in which case + the predicate’s body needs to be tested on the heading). If the + regexp guarantees a match, this may be simply ‘t’, leaving the + query expression with no work to do, which improves performance. + + For convenience, within the ‘pcase’ patterns, the symbol + ‘predicate-names’ is a special form which is replaced with a + pattern matching any of the predicate’s name and aliases. For + example, if ‘NAME’ were: + + ‘(heading h)’ + + Then if ‘NORMALIZERS’ were: + + ‘((`(,predicate-names . ,args) `(heading ,@args)))’ + + It would be expanded to: + + ‘((`(,(or 'heading 'h) . ,args) `(heading ,@args)))’ +  File: README.info, Node: Dynamic block, Next: Links, Prev: Functions / Macros, Up: Usage @@ -859,7 +940,15 @@ File: README.info, Node: 06-pre, Next: 05, Up: Changelog 5.1 0.6-pre =========== -Nothing new yet. +*Added* + • Macro org-ql-defpred, used to define search predicates. (See + tutorial (examples/defpred.org).) + + *Internal* + • Predicates are now defined more cleanly with a macro + (org-ql-defpred) that consolidates functionality related to each + predicate. This will also allow users to more easily define custom + predicates.  File: README.info, Node: 05, Next: 049, Prev: 06-pre, Up: Changelog @@ -1348,54 +1437,55 @@ GPLv3  Tag Table: Node: Top225 -Node: Contents1606 -Node: Screenshots1729 -Node: Installation1847 -Node: Quelpa2485 -Node: Usage2928 -Node: Commands3326 -Node: org-ql-search3799 -Node: helm-org-ql5449 -Node: org-ql-view5861 -Node: org-ql-view-sidebar6361 -Node: org-ql-view-recent-items6717 -Node: org-ql-sparse-tree7201 -Node: Queries8001 -Node: Non-sexp query syntax9112 -Node: General predicates10819 -Node: Ancestor/descendant predicates16232 -Node: Date/time predicates17360 -Node: Functions / Macros20015 -Node: Agenda-like views20270 -Node: Listing / acting-on results21675 -Node: Dynamic block26870 -Node: Links29568 -Node: Tips30255 -Node: Changelog30573 -Node: 06-pre31269 -Node: 0531375 -Node: 04932852 -Node: 04833126 -Node: 04733473 -Node: 04633868 -Node: 04534268 -Node: 04434627 -Node: 04334984 -Node: 04235179 -Node: 04135340 -Node: 0435581 -Node: 03239514 -Node: 03139893 -Node: 0340090 -Node: 02343065 -Node: 02243293 -Node: 02143561 -Node: 0243760 -Node: 0147795 -Node: Notes47896 -Node: Comparison with Org Agenda searches48058 -Node: org-sidebar48930 -Node: License49209 +Node: Contents1628 +Node: Screenshots1751 +Node: Installation1869 +Node: Quelpa2507 +Node: Usage2950 +Node: Commands3348 +Node: org-ql-search3821 +Node: helm-org-ql5471 +Node: org-ql-view5883 +Node: org-ql-view-sidebar6383 +Node: org-ql-view-recent-items6739 +Node: org-ql-sparse-tree7223 +Node: Queries8023 +Node: Non-sexp query syntax9134 +Node: General predicates10841 +Node: Ancestor/descendant predicates16254 +Node: Date/time predicates17382 +Node: Functions / Macros20037 +Node: Agenda-like views20335 +Node: Listing / acting-on results21740 +Node: Custom predicates26961 +Node: Dynamic block30452 +Node: Links33150 +Node: Tips33837 +Node: Changelog34155 +Node: 06-pre34851 +Node: 0535291 +Node: 04936768 +Node: 04837042 +Node: 04737389 +Node: 04637784 +Node: 04538184 +Node: 04438543 +Node: 04338900 +Node: 04239095 +Node: 04139256 +Node: 0439497 +Node: 03243430 +Node: 03143809 +Node: 0344006 +Node: 02346981 +Node: 02247209 +Node: 02147477 +Node: 0247676 +Node: 0151711 +Node: Notes51812 +Node: Comparison with Org Agenda searches51974 +Node: org-sidebar52846 +Node: License53125  End Tag Table diff --git a/tests/test-org-ql.el b/tests/test-org-ql.el index c9d560e..3aebd3e 100644 --- a/tests/test-org-ql.el +++ b/tests/test-org-ql.el @@ -50,7 +50,7 @@ Set at runtime by test suite.") ('org-ql 'org-ql) ('org-ql-expect t) ('org-ql--query-preamble 'query-preamble) - ('org-ql--pre-process-query t) + ('org-ql--normalize-query t) (_ nil))) (result (pcase sexp (`(org-ql-expect ,args) @@ -63,7 +63,7 @@ Set at runtime by test suite.") :action (org-ql-test-org-get-heading)))) (`(org-ql-select . _) (org-ql-test--format-result--ql sexp)) (`(org-ql--query-preamble . _) (org-ql-test--format-result--query-preamble sexp)) - (`(org-ql--pre-process-query . _) (format "'%S" (eval sexp))) + (`(org-ql--normalize-query . _) (format "'%S" (eval sexp))) (_ nil)))) (progn (backward-char 1) @@ -222,61 +222,75 @@ RESULTS should be a list of strings as returned by (describe "(level)" (it "with one level" - (expect (org-ql--pre-process-query '(level "1")) + (expect (org-ql--normalize-query '(level "1")) :to-equal '(level 1))) (it "with two levels" - (expect (org-ql--pre-process-query '(level "1" "2")) + (expect (org-ql--normalize-query '(level "1" "2")) :to-equal '(level 1 2))) (it "with a comparator and a level" - (expect (org-ql--pre-process-query '(level ">" "1")) + (expect (org-ql--normalize-query '(level ">" "1")) :to-equal '(level > 1)))) (describe "(link)" (it "with one argument" - (expect (org-ql--pre-process-query '(link "DESC-OR-TARGET")) + (expect (org-ql--normalize-query '(link "DESC-OR-TARGET")) :to-equal '(link "DESC-OR-TARGET"))) (it "with one argument and :regexp-p" - (expect (org-ql--pre-process-query '(link "DESC-OR-TARGET" :regexp-p t)) + (expect (org-ql--normalize-query '(link "DESC-OR-TARGET" :regexp-p t)) :to-equal '(link "DESC-OR-TARGET" :regexp-p t))) (it "with keyword arguments" - (expect (org-ql--pre-process-query '(link :description "DESCRIPTION" :target "TARGET" - :regexp-p t)) + (expect (org-ql--normalize-query '(link :description "DESCRIPTION" :target "TARGET" + :regexp-p t)) :to-equal '(link :description "DESCRIPTION" :target "TARGET" :regexp-p t)))) - (expect (org-ql--pre-process-query '(and "string1" "string2")) + (expect (org-ql--normalize-query '(and "string1" "string2")) :to-equal '(and (regexp "string1") (regexp "string2"))) - (expect (org-ql--pre-process-query '(or "string1" "string2")) + (expect (org-ql--normalize-query '(or "string1" "string2")) :to-equal '(or (regexp "string1") (regexp "string2"))) - (expect (org-ql--pre-process-query '(and (todo "TODO") - (or "string1" "string2"))) + (expect (org-ql--normalize-query '(and (todo "TODO") + (or "string1" "string2"))) :to-equal '(and (todo "TODO") (or (regexp "string1") (regexp "string2")))) - (expect (org-ql--pre-process-query '(when (todo "TODO") - (or "string1" "string2"))) + (expect (org-ql--normalize-query '(when (todo "TODO") + (or "string1" "string2"))) :to-equal '(when (todo "TODO") (or (regexp "string1") (regexp "string2")))) - (expect (org-ql--pre-process-query '(when "string-cond1" - (or "string1" "string2"))) + (expect (org-ql--normalize-query '(when "string-cond1" + (or "string1" "string2"))) :to-equal '(when (regexp "string-cond1") (or (regexp "string1") (regexp "string2")))) - (expect (org-ql--pre-process-query '(when (and "string-cond1" "string-cond2") - (or "string1" "string2"))) + (expect (org-ql--normalize-query '(when (and "string-cond1" "string-cond2") + (or "string1" "string2"))) :to-equal '(when (and (regexp "string-cond1") (regexp "string-cond2")) (or (regexp "string1") (regexp "string2")))) - (expect (org-ql--pre-process-query '(unless (and "stringcondition1" "stringcond2") - (or "string1" "string2"))) + (expect (org-ql--normalize-query '(unless (and "stringcondition1" "stringcond2") + (or "string1" "string2"))) :to-equal '(unless (and (regexp "stringcondition1") (regexp "stringcond2")) (or (regexp "string1") (regexp "string2")))) - (expect (org-ql--pre-process-query '(or (ts-active :on "2019-01-01") - (ts-a :on "2019-01-01") - (ts-inactive :on "2019-01-01") - (ts-i :on "2019-01-01"))) + (expect (org-ql--normalize-query '(or (ts-active :on "2019-01-01") + (ts-a :on "2019-01-01") + (ts-inactive :on "2019-01-01") + (ts-i :on "2019-01-01"))) :to-equal '(or (ts :type active :on "2019-01-01") (ts :type active :on "2019-01-01") (ts :type inactive :on "2019-01-01") (ts :type inactive :on "2019-01-01")))) - (describe "Query optimizing" + (describe "Query preambles" ;; TODO: Other predicates. + (describe "(clocked)" + (it "without arguments" + (expect (org-ql--query-preamble '(clocked)) + :to-equal (list :query t + :preamble org-ql-clock-regexp + :preamble-case-fold nil))) + (it "with a number of days" + (expect (org-ql--query-preamble '(clocked 1)) + :to-equal (list :query t + :preamble org-ql-clock-regexp + :preamble-case-fold nil))) + ;; TODO: Other arguments for (clocked). + ) + (describe "(level)" (it "with a number" (expect (org-ql--query-preamble '(level 2)) @@ -314,60 +328,60 @@ RESULTS should be a list of strings as returned by ;; TODO: Other predicates. (it "Negated terms" - (expect (org-ql--plain-query "todo: !todo:CHECK,SOMEDAY") + (expect (org-ql--query-string-to-sexp "todo: !todo:CHECK,SOMEDAY") :to-equal '(and (todo) (not (todo "CHECK" "SOMEDAY")))) - (expect (org-ql--plain-query "!todo:CHECK,SOMEDAY todo:") + (expect (org-ql--query-string-to-sexp "!todo:CHECK,SOMEDAY todo:") :to-equal '(and (not (todo "CHECK" "SOMEDAY")) (todo))) - (expect (org-ql--plain-query "tags:universe !moon") + (expect (org-ql--query-string-to-sexp "tags:universe !moon") :to-equal '(and (tags "universe") (not (regexp "moon")))) - (expect (org-ql--plain-query "!moon tags:universe") + (expect (org-ql--query-string-to-sexp "!moon tags:universe") :to-equal '(and (not (regexp "moon")) (tags "universe"))) - (expect (org-ql--plain-query "mars !ts:on=today") + (expect (org-ql--query-string-to-sexp "mars !ts:on=today") :to-equal '(and (regexp "mars") (not (ts :on "today")))) - (expect (org-ql--plain-query "!\"quoted phrase\"") + (expect (org-ql--query-string-to-sexp "!\"quoted phrase\"") :to-equal '(not (regexp "quoted phrase")))) (it "Regexp predicates" - (expect (org-ql--plain-query "scheduled") + (expect (org-ql--query-string-to-sexp "scheduled") ;; No colon after keyword, so not a predicate query. :to-equal '(regexp "scheduled")) - (expect (org-ql--plain-query "\"quoted phrase\"") + (expect (org-ql--query-string-to-sexp "\"quoted phrase\"") :to-equal '(regexp "quoted phrase")) - (expect (org-ql--plain-query "regexp:word") + (expect (org-ql--query-string-to-sexp "regexp:word") :to-equal '(regexp "word")) - (expect (org-ql--plain-query "regexp:\"quoted phrase\"") + (expect (org-ql--query-string-to-sexp "regexp:\"quoted phrase\"") :to-equal '(regexp "quoted phrase"))) (it "Timestamp-based predicates" - (expect (org-ql--plain-query "scheduled:on=2017-07-07") + (expect (org-ql--query-string-to-sexp "scheduled:on=2017-07-07") :to-equal '(scheduled :on "2017-07-07")) - (expect (org-ql--plain-query "deadline:from=2017-07-07,to=2017-07-09") + (expect (org-ql--query-string-to-sexp "deadline:from=2017-07-07,to=2017-07-09") :to-equal '(deadline :from "2017-07-07" :to "2017-07-09")) - (expect (org-ql--plain-query "planning:from=2017-07-07") + (expect (org-ql--query-string-to-sexp "planning:from=2017-07-07") :to-equal '(planning :from "2017-07-07")) - (expect (org-ql--plain-query "closed:from=2017-07-07") + (expect (org-ql--query-string-to-sexp "closed:from=2017-07-07") :to-equal '(closed :from "2017-07-07")) - (expect (org-ql--plain-query "ts-active:to=2017-07-07") + (expect (org-ql--query-string-to-sexp "ts-active:to=2017-07-07") :to-equal '(ts-active :to "2017-07-07")) - (expect (org-ql--plain-query "ts-inactive:to=2017-07-07") + (expect (org-ql--query-string-to-sexp "ts-inactive:to=2017-07-07") :to-equal '(ts-inactive :to "2017-07-07")) - (expect (org-ql--plain-query "ts-a:to=2017-07-07") + (expect (org-ql--query-string-to-sexp "ts-a:to=2017-07-07") :to-equal '(ts-a :to "2017-07-07")) - (expect (org-ql--plain-query "ts-i:on=2017-07-07") + (expect (org-ql--query-string-to-sexp "ts-i:on=2017-07-07") :to-equal '(ts-i :on "2017-07-07")) - (expect (org-ql--plain-query "ts:") + (expect (org-ql--query-string-to-sexp "ts:") :to-equal '(ts)) - (expect (org-ql--plain-query "clocked:") + (expect (org-ql--query-string-to-sexp "clocked:") :to-equal '(clocked))) (it "To-do predicates" - (expect (org-ql--plain-query "todo:") + (expect (org-ql--query-string-to-sexp "todo:") :to-equal '(todo)) - (expect (org-ql--plain-query "todo:TODO") + (expect (org-ql--query-string-to-sexp "todo:TODO") :to-equal '(todo "TODO")) - (expect (org-ql--plain-query "todo:TODO,SOMEDAY") + (expect (org-ql--query-string-to-sexp "todo:TODO,SOMEDAY") :to-equal '(todo "TODO" "SOMEDAY"))) (it "Compound queries" - (expect (org-ql--plain-query "todo:SOMEDAY ts-a:from=2020-01-01,to=2021-01-01") + (expect (org-ql--query-string-to-sexp "todo:SOMEDAY ts-a:from=2020-01-01,to=2021-01-01") :to-equal '(and (todo "SOMEDAY") (ts-a :from "2020-01-01" :to "2021-01-01"))) - (expect (org-ql--plain-query "regexp:\"quoted phrase\" todo:SOMEDAY") + (expect (org-ql--query-string-to-sexp "regexp:\"quoted phrase\" todo:SOMEDAY") :to-equal '(and (regexp "quoted phrase") (todo "SOMEDAY"))))) (describe "Convert sexp queries to non-sexp queries" @@ -842,9 +856,9 @@ RESULTS should be a list of strings as returned by (org-ql-expect ('(scheduled :to today)) '("Skype with president of Antarctica" "Practice leaping tall buildings in a single bound" "Order a pizza" "Get haircut" "Fix flux capacitor" "Shop for groceries" "Rewrite Emacs in Common Lisp"))))) - ;; TODO: Test (src) predicate. That will require modifying test data, which will be a - ;; significant hassle. Manual testing shows that the predicate appears to work properly. - + ;; ;; TODO: Test (src) predicate. That will require modifying test data, which will be a + ;; ;; significant hassle. Manual testing shows that the predicate appears to work properly. + ;; (describe "(todo)" (org-ql-it "without arguments" From a2acf93a2c4fdcfd4752aa3c71d946b882ad8a09 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Mon, 23 Nov 2020 01:33:14 -0600 Subject: [PATCH 074/372] Docs: (defpred.org) Fix example --- examples/defpred.org | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/defpred.org b/examples/defpred.org index 86565cf..75c28b7 100644 --- a/examples/defpred.org +++ b/examples/defpred.org @@ -164,7 +164,7 @@ Can we do that? In fact, we can, by using a query normalizer. Normalizers are collect `(property "person" ,name))))) :body (cl-loop for name in names thereis (or (property "person" name) - (tags (concat "person" name))))) + (tags name)))) #+END_SRC Now, don't faint from all the backquoting and unquoting--it's just Lisp, nothing to be afraid of! Let's slow down a moment and see what the normalized query looks like to be sure we're doing it correctly: From 485bbdfda8723722fbd2b39fe15a852ef5a99703 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Mon, 23 Nov 2020 02:03:48 -0600 Subject: [PATCH 075/372] Meta: Upgrade makem.sh, Makefile, test.yml --- .github/workflows/test.yml | 2 +- Makefile | 2 +- makem.sh | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0b274ed..d5ec8cc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,7 +1,7 @@ # * test.yml --- Test Emacs packages using makem.sh on GitHub Actions # URL: https://github.com/alphapapa/makem.sh -# Version: 0.2 +# Version: 0.2.1 # * Commentary: diff --git a/Makefile b/Makefile index 68906e9..4abfce9 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # * makem.sh/Makefile --- Script to aid building and testing Emacs Lisp packages # URL: https://github.com/alphapapa/makem.sh -# Version: 0.2 +# Version: 0.2.1 # * Arguments diff --git a/makem.sh b/makem.sh index 2d710a3..0cc3cbc 100755 --- a/makem.sh +++ b/makem.sh @@ -3,7 +3,7 @@ # * makem.sh --- Script to aid building and testing Emacs Lisp packages # URL: https://github.com/alphapapa/makem.sh -# Version: 0.2 +# Version: 0.2.1 # * Commentary: @@ -422,8 +422,9 @@ function package-main-file { function dependencies { # Echo list of package dependencies. - # Search package headers. - egrep -i '^;; Package-Requires: ' $(files-project-feature) $(files-project-test) \ + # Search package headers. Use -a so grep won't think that an Elisp file containing + # control characters (rare, but sometimes necessary) is binary and refuse to search it. + egrep -a -i '^;; Package-Requires: ' $(files-project-feature) $(files-project-test) \ | egrep -o '\([^([:space:]][^)]*\)' \ | egrep -o '^[^[:space:])]+' \ | sed -r 's/\(//g' \ From a24de862ec848568ffae1ed3fb2d515dca0dc6b1 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Mon, 23 Nov 2020 02:03:58 -0600 Subject: [PATCH 076/372] Fix: (org-ql-defpred) Use alist-get Until map-2.1 can automatically be installed with makem.sh... --- org-ql.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/org-ql.el b/org-ql.el index b4b98b3..499e0b1 100644 --- a/org-ql.el +++ b/org-ql.el @@ -990,7 +990,8 @@ It would be expanded to: `(progn (cl-eval-when (compile load eval) (cl-defun ,fn-name ,args ,docstring ,body)) - (setf (map-elt org-ql-predicates ',predicate-name) + ;; SOMEDAY: Use `map-elt' here, after map 2.1 can be automatically installed in CI sandbox... + (setf (alist-get ',predicate-name org-ql-predicates) `(:name ,',name :aliases ,',aliases :fn ,',fn-name :docstring ,,docstring :args ,',args :normalizers ,',normalizers :preambles ,',preambles)) (unless org-ql-defpred-defer From 58bf5dc0a38050df6fc9494334a602181743b0bc Mon Sep 17 00:00:00 2001 From: "Zen Monk Alain M. Lafon" Date: Mon, 23 Nov 2020 09:33:44 +0100 Subject: [PATCH 077/372] Proofread `defpred` tutorial --- examples/defpred.org | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/defpred.org b/examples/defpred.org index 75c28b7..d5fc7c5 100644 --- a/examples/defpred.org +++ b/examples/defpred.org @@ -3,7 +3,7 @@ [2020-11-22 Sun 22:35] Imagine you have weekly meetings with other people, and during the week you take notes about items to discuss at each meeting. When the time for a meeting comes, you want to quickly and easily search for all of the items to discuss at the meeting. -You've been experimenting with different ways to track such data in Org. You've tried using tags, but some of the names in question conflict with other tags in your data (e.g. someone's named Charles, but you also work with a firm named Charles, Inc., and you'd prefer to continue using the tag =Charles= for entries about that firm), so you've been using tags like ~:personNAME:~, which seems awkward. You've tried using a ~:person: NAME~ property on entries, which has the advantage of not cluttering the tags list, but also the disadvantage of not being readily visible in an outline. +You've been experimenting with different ways to track such data in Org. You've tried using tags, but some of the names in question conflict with other tags in your data (e.g. someone's named Charles, but you also work with a firm named Charles, Inc., and you'd prefer to continue using the tag =Charles= for entries about that firm), so you've been using tags like ~:personNAME:~, which seems awkward. You've tried using a ~:person: NAME~ property on entries, which has the advantage of not cluttering the tags list, but also the disadvantage of not being readily visible in an outline. So you haven't decided on a long-term solution, but the meetings aren't going to wait--you need to search that data now, and you have a mix of both tags and properties in your entries. What you need is to be able to search for all of the entries about Alice (which you've tagged ~:personAlice:~) when you're meeting with her, and all of the entries about Bob (which have the property ~:person: Bob~) when you're meeting with him What do you do? @@ -51,7 +51,7 @@ Enter =org-ql= custom search predicates. Let's start simple, by defining a pred :body (property "person" name)) #+END_SRC -Now let's see what results we get for searching this file for entries about Bob: +Now, let's see what results we get for searching this file for entries about Bob: #+BEGIN_SRC elisp :results list :exports both :cache yes (org-ql-query :select '(org-get-heading :no-tags) @@ -77,7 +77,7 @@ Hmm, looks like we need to remind Bob to wash his mug and take some mints after #+END_EXAMPLE -Nothing? Oh, right, Alice's entries use the ~:personAlice:~ tag, so we'll also need to also search those kind of entries. Let's make the predicate do that too: +Nothing? Oh, right, Alice's entries use the ~:personAlice:~ tag, so we'll also need to search those kind of entries. Let's make the predicate do that, too: #+BEGIN_SRC elisp :results silent :exports code (org-ql-defpred person (name) From a06ea62521c2ebbb0cdafb78f83a01896414ce24 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Mon, 23 Nov 2020 02:59:43 -0600 Subject: [PATCH 078/372] Docs: (defpred.org) Tidy --- examples/defpred.org | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/defpred.org b/examples/defpred.org index d5fc7c5..3cdf07f 100644 --- a/examples/defpred.org +++ b/examples/defpred.org @@ -204,7 +204,7 @@ But wait, that's not all! If you order now, we'll throw in non-sexp query synta (org-ql-search (current-buffer) "person:Alice,Bob") #+END_SRC -Don't believe me? Well, you see, queries in this syntax are automatically converted to the sexp syntax, like: +Don't believe me? Well, you see, queries in this syntax are converted to the sexp syntax, like: #+BEGIN_SRC elisp :results code :exports both :cache yes (org-ql--query-string-to-sexp "person:Alice,Bob") @@ -215,7 +215,7 @@ Don't believe me? Well, you see, queries in this syntax are automatically conve (person "Alice" "Bob") #+END_SRC -But that happens automatically when you use a search command like =org-ql-search=. If you have =org-ql= installed already, you could even click this link: [[org-ql-search:person:Alice,Bob][Alice or Bob]]. Which, in Org syntax, looks like: +And that happens automatically when you use a search command like =org-ql-search=. If you have =org-ql= installed already, you could even click this link: [[org-ql-search:person:Alice,Bob][Alice or Bob]]. Which, in Org syntax, looks like: #+BEGIN_SRC org [[org-ql-search:person:Alice,Bob]] @@ -286,8 +286,8 @@ Finally, if you're a Lisper who appreciates anaphora, you might prefer a more sy :normalizers ((`(,predicate-names . ,names) `(or (tags ,@(--map `(concat "person" ,it) names)) ,@(--map `(property "person" ,it) names)))) - :body (--any (or (property "person" name) - (tags (concat "person" name))) + :body (--any (or (property "person" it) + (tags (concat "person" it))) names)) #+END_SRC From b01106bd0f73c5eaea55a8d41a6943977363f356 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Mon, 23 Nov 2020 03:10:10 -0600 Subject: [PATCH 079/372] Docs: (defpred.org) Formless, conclusion --- examples/defpred.org | 47 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/examples/defpred.org b/examples/defpred.org index 3cdf07f..53dac32 100644 --- a/examples/defpred.org +++ b/examples/defpred.org @@ -5,7 +5,7 @@ You've been experimenting with different ways to track such data in Org. You've tried using tags, but some of the names in question conflict with other tags in your data (e.g. someone's named Charles, but you also work with a firm named Charles, Inc., and you'd prefer to continue using the tag =Charles= for entries about that firm), so you've been using tags like ~:personNAME:~, which seems awkward. You've tried using a ~:person: NAME~ property on entries, which has the advantage of not cluttering the tags list, but also the disadvantage of not being readily visible in an outline. -So you haven't decided on a long-term solution, but the meetings aren't going to wait--you need to search that data now, and you have a mix of both tags and properties in your entries. What you need is to be able to search for all of the entries about Alice (which you've tagged ~:personAlice:~) when you're meeting with her, and all of the entries about Bob (which have the property ~:person: Bob~) when you're meeting with him What do you do? +So you haven't decided on a long-term solution, but the meetings aren't going to wait--you need to search that data now, and you have a mix of both tags and properties in your entries. What you need is to be able to search for all of the entries about Alice (which you've tagged ~:personAlice:~) when you're meeting with her, and all of the entries about Bob (which have the property ~:person: Bob~) when you're meeting with him. What do you do? * Contents :PROPERTIES: @@ -19,6 +19,8 @@ So you haven't decided on a long-term solution, but the meetings aren't going to - [[#non-sexp-query-syntax][Non-sexp query syntax]] - [[#using-multiple-predicates][Using multiple predicates]] - [[#predicate-aliases][Predicate aliases]] +- [[#be-formless][Be formless]] +- [[#conclusion][Conclusion]] - [[#appendix-anaphoric-macros][Appendix: Anaphoric macros]] :END: @@ -276,6 +278,42 @@ And, you know what, if you're just so busy that you don't even have time to type (It's up to you to remember whether =p= means =person= or =priority=, but code can't solve everything.) +* Be formless + +We can even go a step further: since the normalizer rewrites the query to call the =property= and =tags= predicates instead, this =person= predicate doesn't even need a body form! + +#+BEGIN_SRC elisp :results silent :exports code + (org-ql-defpred (person p) (&rest names) + "Search for entries about any of NAMES." + :normalizers ((`(,predicate-names . ,names) + `(or (tags ,@(cl-loop for name in names + collect (concat "person" name))) + ,@(cl-loop for name in names + collect `(property "person" ,name)))))) +#+END_SRC + +Will it still work? + +#+BEGIN_SRC elisp :results list :exports both :cache yes + (org-ql-query :select '(org-get-heading :no-tags) + :from (current-buffer) + :where '(person "Alice" "Bob")) +#+END_SRC + +#+RESULTS[4f5971c56616f01d8d3c28a66ef380495ee3e158]: +- [#A] Loud pet parakeet +- [#C] Missing sticky notes +- [#C] Dirty dishes in sink +- [#A] Stinky coffee breath + +It does! + +* Conclusion + +In this tutorial, we've gone from having to write lengthy, complex query expressions for accommodating idiosyncratic requirements, to being able to write simple query expressions that abstract away ugly details, to rewriting those query expressions into a more optimal form before a search is even run. The end result is an Org Query Language that is customized to meet your specific needs. + +What new custom predicates will you write next? + * Appendix: Anaphoric macros Finally, if you're a Lisper who appreciates anaphora, you might prefer a more syntactically concise definition of the predicate using Dash macros: @@ -285,10 +323,7 @@ Finally, if you're a Lisper who appreciates anaphora, you might prefer a more sy "Search for entries about any of NAMES." :normalizers ((`(,predicate-names . ,names) `(or (tags ,@(--map `(concat "person" ,it) names)) - ,@(--map `(property "person" ,it) names)))) - :body (--any (or (property "person" it) - (tags (concat "person" it))) - names)) + ,@(--map `(property "person" ,it) names))))) #+END_SRC Let's make sure it works: @@ -305,7 +340,7 @@ Let's make sure it works: - [#C] Dirty dishes in sink - [#A] Stinky coffee breath -Have fun making custom search predicates! +Lisp is fun! * Example data :PROPERTIES: From 92d099caf61c87a712117fffee618485885679eb Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Mon, 23 Nov 2020 03:19:13 -0600 Subject: [PATCH 080/372] Docs: (defpred.org) Add HTML export It looks nice, even if it isn't practically viewable on GitHub. --- examples/defpred.html | 766 ++++++++++++++++++ examples/defpred.org | 13 +- .../export/setup/theme-darksun-local.setup | 17 + examples/export/setup/theme-darksun.setup | 15 + .../export/styles/darksun/css/darksun.css | 264 ++++++ .../export/styles/darksun/css/hideshow.css | 110 +++ .../export/styles/darksun/css/htmlize.css | 145 ++++ .../export/styles/darksun/images/checked.png | Bin 0 -> 3136 bytes .../export/styles/darksun/images/fixme.png | Bin 0 -> 625 bytes .../styles/darksun/images/halfchecked.png | Bin 0 -> 3080 bytes .../export/styles/darksun/images/info.png | Bin 0 -> 4403 bytes .../export/styles/darksun/images/note.png | Bin 0 -> 4567 bytes examples/export/styles/darksun/images/tip.png | Bin 0 -> 4447 bytes .../styles/darksun/images/unchecked.png | Bin 0 -> 3087 bytes .../export/styles/darksun/images/warning.png | Bin 0 -> 4340 bytes examples/export/styles/darksun/js/darksun.js | 524 ++++++++++++ examples/export/styles/darksun/js/hideshow.js | 335 ++++++++ .../styles/darksun/js/jquery-1.11.0.min.js | 4 + .../styles/darksun/js/jquery-1.9.1.min.js | 5 + .../styles/darksun/js/jquery-ui-1.10.2.min.js | 12 + .../darksun/js/jquery.localscroll-min.js | 9 + .../darksun/js/jquery.scrollTo-1.4.3.1-min.js | 7 + .../styles/darksun/js/jquery.zclip.min.js | 12 + .../lib/js/jquery.stickytableheaders.min.js | 1 + .../lib/js/stickytableheaders-license.txt | 20 + 25 files changed, 2257 insertions(+), 2 deletions(-) create mode 100644 examples/defpred.html create mode 100644 examples/export/setup/theme-darksun-local.setup create mode 100644 examples/export/setup/theme-darksun.setup create mode 100644 examples/export/styles/darksun/css/darksun.css create mode 100644 examples/export/styles/darksun/css/hideshow.css create mode 100644 examples/export/styles/darksun/css/htmlize.css create mode 100644 examples/export/styles/darksun/images/checked.png create mode 100644 examples/export/styles/darksun/images/fixme.png create mode 100644 examples/export/styles/darksun/images/halfchecked.png create mode 100644 examples/export/styles/darksun/images/info.png create mode 100644 examples/export/styles/darksun/images/note.png create mode 100644 examples/export/styles/darksun/images/tip.png create mode 100644 examples/export/styles/darksun/images/unchecked.png create mode 100644 examples/export/styles/darksun/images/warning.png create mode 100644 examples/export/styles/darksun/js/darksun.js create mode 100644 examples/export/styles/darksun/js/hideshow.js create mode 100644 examples/export/styles/darksun/js/jquery-1.11.0.min.js create mode 100644 examples/export/styles/darksun/js/jquery-1.9.1.min.js create mode 100644 examples/export/styles/darksun/js/jquery-ui-1.10.2.min.js create mode 100644 examples/export/styles/darksun/js/jquery.localscroll-min.js create mode 100644 examples/export/styles/darksun/js/jquery.scrollTo-1.4.3.1-min.js create mode 100644 examples/export/styles/darksun/js/jquery.zclip.min.js create mode 100644 examples/export/styles/lib/js/jquery.stickytableheaders.min.js create mode 100644 examples/export/styles/lib/js/stickytableheaders-license.txt diff --git a/examples/defpred.html b/examples/defpred.html new file mode 100644 index 0000000..9c9b57e --- /dev/null +++ b/examples/defpred.html @@ -0,0 +1,766 @@ + + + + + + + +Org QL Custom Predicates Tutorial + + + + + + + + + + + + + +
+

Org QL Custom Predicates Tutorial

+ +

+[2020-11-22 Sun 22:35] Imagine you have weekly meetings with other people, and during the week you take notes about items to discuss at each meeting. When the time for a meeting comes, you want to quickly and easily search for all of the items to discuss at the meeting. +

+ +

+You've been experimenting with different ways to track such data in Org. You've tried using tags, but some of the names in question conflict with other tags in your data (e.g. someone's named Charles, but you also work with a firm named Charles, Inc., and you'd prefer to continue using the tag Charles for entries about that firm), so you've been using tags like :personNAME:, which seems awkward. You've tried using a :person: NAME property on entries, which has the advantage of not cluttering the tags list, but also the disadvantage of not being readily visible in an outline. +

+ +

+So you haven't decided on a long-term solution, but the meetings aren't going to wait–you need to search that data now, and you have a mix of both tags and properties in your entries. What you need is to be able to search for all of the entries about Alice (which you've tagged :personAlice:) when you're meeting with her, and all of the entries about Bob (which have the property :person: Bob) when you're meeting with him. What do you do? +

+ +
+

Using built-in predicates

+
+

+You could start by using built-in Org QL predicates to search your data. For example: +

+ +
+
(org-ql-query :select '(org-get-heading :no-tags)
+              :from (current-buffer)
+              :where '(or (tags "personAlice")
+                          (property "person" "Bob")))
+
+
+ +
    +
  • [#A] Loud pet parakeet
  • +
  • [#C] Missing sticky notes
  • +
  • [#C] Dirty dishes in sink
  • +
  • [#A] Stinky coffee breath
  • +
+ +

+That was easy enough, but it's not very…semantic. You have to think about the implementation details: Alice uses tags, Bob uses properties, and what if Charlie uses both? It starts to feel complicated, and it's a lot to type out every time. Is there an easier way? +

+
+
+ +
+

A custom (person) predicate

+
+

+Enter org-ql custom search predicates. Let's start simple, by defining a predicate to search for just the :person: property, one person at a time. The predicate will take one argument, a person's name, and search for that property. It would look like this: +

+ +
+
(org-ql-defpred person (name)
+  "Search for entries with the \"person\" property being NAME."
+  :body (property "person" name))
+
+
+ +

+Now, let's see what results we get for searching this file for entries about Bob: +

+ +
+
(org-ql-query :select '(org-get-heading :no-tags)
+              :from (current-buffer)
+              :where '(person "Bob"))
+
+
+ +
    +
  • [#C] Missing sticky notes
  • +
  • [#C] Dirty dishes in sink
  • +
  • [#A] Stinky coffee breath
  • +
+ +

+Hmm, looks like we need to remind Bob to wash his mug and take some mints after lunch. Now what do we need to discuss with Alice? +

+ +
+
(org-ql-query :select '(org-get-heading :no-tags)
+              :from (current-buffer)
+              :where '(person "Alice"))
+
+
+ +
+
+ +

+Nothing? Oh, right, Alice's entries use the :personAlice: tag, so we'll also need to search those kind of entries. Let's make the predicate do that, too: +

+ +
+
(org-ql-defpred person (name)
+  "Search for entries with the \"person\" property being NAME or having the tag \"personNAME\"."
+  :body (or (property "person" name)
+            (tags (concat "person" name))))
+
+
+ +

+How about now? +

+ +
+
(org-ql-query :select '(org-get-heading :no-tags)
+              :from (current-buffer)
+              :where '(person "Alice"))
+
+
+ +
    +
  • [#A] Loud pet parakeet
  • +
  • [#C] Missing sticky notes
  • +
  • [#C] Dirty dishes in sink
  • +
+ +

+Hmm, I thought we already told her to leave Polly at home. +

+
+
+ +
+

Searching for multiple people at once

+
+

+Oh, wait, this week is shortened due to holidays, so we're having a combined meeting. How do we search for entries about either of them? Well, this is the obvious solution: +

+ +
+
(org-ql-query :select '(org-get-heading :no-tags)
+              :from (current-buffer)
+              :where '(or (person "Alice")
+                          (person "Bob")))
+
+
+ +
    +
  • [#A] Loud pet parakeet
  • +
  • [#C] Missing sticky notes
  • +
  • [#C] Dirty dishes in sink
  • +
  • [#A] Stinky coffee breath
  • +
+ +

+And that works fine. But it seems like a lot to type. Could we make the person predicate accept multiple names instead? +

+ +
+
(org-ql-defpred person (&rest names)
+  "Search for entries about any of NAMES."
+  :body (cl-loop for name in names
+                 thereis (or (property "person" name)
+                             (tags (concat "person" name)))))
+
+
+ +

+Now let's search again: +

+ +
+
(org-ql-query :select '(org-get-heading :no-tags)
+              :from (current-buffer)
+              :where '(person "Alice" "Bob"))
+
+
+ +
    +
  • [#A] Loud pet parakeet
  • +
  • [#C] Missing sticky notes
  • +
  • [#C] Dirty dishes in sink
  • +
  • [#A] Stinky coffee breath
  • +
+ +

+That was easy! +

+
+
+ +
+

Normalizing queries to rewrite arguments

+
+

+Now, all this is well and good if you don't have hundreds of thousands of Org entries in your files. But what if you do? All that concat'ing happening on every entry could add up, and the query might take a few seconds. What if we could do that stringing-along just once, before running the query? We want to turn our (person "Alice" "Bob") query into this, with the :personNAME: strings already made and the per-person (property ...) predicates also included: +

+ +
+
(or (tags "personAlice" "personBob")
+    (property "person" "Alice")
+    (property "person" "Bob"))
+
+
+ +

+Can we do that? In fact, we can, by using a query normalizer. Normalizers are pcase forms (I know) that normalize query expressions before execution. We can use one to rewrite the query ahead of time, like this: +

+ +
+
(org-ql-defpred person (&rest names)
+  "Search for entries about any of NAMES."
+  :normalizers ((`(person . ,names)
+                 `(or (tags ,@(cl-loop for name in names
+                                       collect (concat "person" name)))
+                      ,@(cl-loop for name in names
+                                 collect `(property "person" ,name)))))
+  :body (cl-loop for name in names
+                 thereis (or (property "person" name)
+                             (tags name))))
+
+
+ +

+Now, don't faint from all the backquoting and unquoting–it's just Lisp, nothing to be afraid of! Let's slow down a moment and see what the normalized query looks like to be sure we're doing it correctly: +

+ +
+
(org-ql--normalize-query '(person "Alice" "Bob"))
+
+
+ +
+
(or (tags "personAlice" "personBob")
+    (property "person" "Alice")
+    (property "person" "Bob"))
+
+
+ +

+And, as they say, Bob's your uncle! Or even if he isn't, let's see if it works: +

+ +
+
(org-ql-query :select '(org-get-heading :no-tags)
+              :from (current-buffer)
+              :where '(person "Alice" "Bob"))
+
+
+ +
    +
  • [#A] Loud pet parakeet
  • +
  • [#C] Missing sticky notes
  • +
  • [#C] Dirty dishes in sink
  • +
  • [#A] Stinky coffee breath
  • +
+ +

+Yep, same result as the non-normalized query. And look at how much simpler it is to write (person "Alice" "Bob") than to write (or (tags "personAlice" "personBob") (property "person" "Alice") (property "person" "Bob")). +

+
+
+ +
+

Non-sexp query syntax

+
+

+But wait, that's not all! If you order now, we'll throw in non-sexp query syntax for free! That's right, your search could be as simple as typing person:Alice,Bob! +

+ +
+
(org-ql-search (current-buffer) "person:Alice,Bob")
+
+
+ +

+Don't believe me? Well, you see, queries in this syntax are converted to the sexp syntax, like: +

+ +
+
(org-ql--query-string-to-sexp "person:Alice,Bob")
+
+
+ +
+
(person "Alice" "Bob")
+
+
+ +

+And that happens automatically when you use a search command like org-ql-search. If you have org-ql installed already, you could even click this link: Alice or Bob. Which, in Org syntax, looks like: +

+ + + +

+And that would open an Agenda Mode buffer that looks like this: +

+ +
+Query: (person "Alice" "Bob")  In:meetings.org
+  [#A] Loud pet parakeet                                           :personAlice:
+  [#C] Missing sticky notes                                        :personAlice:
+  [#C] Dirty dishes in sink                                        :personAlice:
+  [#A] Stinky coffee breath
+
+
+
+ +
+

Using multiple predicates

+
+

+Oops, you forgot that there's a birthday party in 20 minutes, so you only have time to talk about the highest priority items at this joint meeting today. +

+ +

+No problem, let's just select high-priority items: +

+ +
+
(org-ql-search (current-buffer) "person:Alice,Bob priority:A")
+
+
+ +

+Which shows: +

+ +
+Query: (and (person "Alice" "Bob") (priority "A"))  In:meetings.org
+  [#A] Loud pet parakeet                                           :personAlice:
+  [#A] Stinky coffee breath
+
+
+
+ +
+

Predicate aliases

+
+

+And, you know what, if you're just so busy that you don't even have time to type the word person, you can add an abbreviated alias, p, like this: +

+ +
+
(org-ql-defpred (person p) (&rest names)
+  "Search for entries about any of NAMES."
+  :normalizers ((`(,predicate-names . ,names)
+                 `(or (tags ,@(cl-loop for name in names
+                                       collect (concat "person" name)))
+                      ,@(cl-loop for name in names
+                                 collect `(property "person" ,name)))))
+  :body (cl-loop for name in names
+                 thereis (or (property "person" name)
+                             (tags (concat "person" name)))))
+
+
+ +

+Let's try it: +

+ +
+
(org-ql-search (current-buffer) "p:Alice,Bob priority:A")
+
+
+ +

+And that shows: +

+ +
+Query: (and (person "Alice" "Bob") (priority "A"))  In:meetings.org
+  [#A] Loud pet parakeet                                           :personAlice:
+  [#A] Stinky coffee breath
+
+ +

+(It's up to you to remember whether p means person or priority, but code can't solve everything.) +

+
+
+ +
+

Be formless

+
+

+We can even go a step further: since the normalizer rewrites the query to call the property and tags predicates instead, this person predicate doesn't even need a body form! +

+ +
+
(org-ql-defpred (person p) (&rest names)
+  "Search for entries about any of NAMES."
+  :normalizers ((`(,predicate-names . ,names)
+                 `(or (tags ,@(cl-loop for name in names
+                                       collect (concat "person" name)))
+                      ,@(cl-loop for name in names
+                                 collect `(property "person" ,name))))))
+
+
+ +

+Will it still work? +

+ +
+
(org-ql-query :select '(org-get-heading :no-tags)
+              :from (current-buffer)
+              :where '(person "Alice" "Bob"))
+
+
+ +
    +
  • [#A] Loud pet parakeet
  • +
  • [#C] Missing sticky notes
  • +
  • [#C] Dirty dishes in sink
  • +
  • [#A] Stinky coffee breath
  • +
+ +

+It does! +

+
+
+ +
+

Conclusion

+
+

+In this tutorial, we've gone from having to write lengthy, complex query expressions for accommodating idiosyncratic requirements, to being able to write simple query expressions that abstract away ugly details, to rewriting those query expressions into a more optimal form before a search is even run. The end result is an Org Query Language that is customized to meet your specific needs. +

+ +

+What new custom predicates will you write next? +

+
+
+ +
+

Appendix: Anaphoric macros

+
+

+Finally, if you're a Lisper who appreciates anaphora, you might prefer a more syntactically concise definition of the predicate using Dash macros: +

+ +
+
(org-ql-defpred (person p) (&rest names)
+  "Search for entries about any of NAMES."
+  :normalizers ((`(,predicate-names . ,names)
+                 `(or (tags ,@(--map `(concat "person" ,it) names))
+                      ,@(--map `(property "person" ,it) names)))))
+
+
+ +

+Let's make sure it works: +

+ +
+
(org-ql-query :select '(org-get-heading :no-tags)
+              :from (current-buffer)
+              :where '(person "Alice" "Bob"))
+
+
+ +
    +
  • [#A] Loud pet parakeet
  • +
  • [#C] Missing sticky notes
  • +
  • [#C] Dirty dishes in sink
  • +
  • [#A] Stinky coffee breath
  • +
+ +

+Lisp is fun! +

+
+
+ +
+

Example data

+
+
+ +
+

Loud pet parakeet   personAlice

+
+ +
+

Missing sticky notes   personAlice

+
+
+
+ +
+

Dirty dishes in sink   personAlice

+
+
+
+ +
+

Stinky coffee breath

+
+
+
+
+
+
+

Created: 2020-11-23 Mon 03:31

+

Validate

+
+ + diff --git a/examples/defpred.org b/examples/defpred.org index 53dac32..4f19576 100644 --- a/examples/defpred.org +++ b/examples/defpred.org @@ -1,5 +1,6 @@ #+TITLE: Org QL Custom Predicates Tutorial -#+OPTIONS: author:nil creator:nil created:nil date:nil num:nil title:t +#+OPTIONS: author:nil creator:nil created:nil date:nil num:nil title:t broken-links:t +#+SETUPFILE: export/setup/theme-darksun-local.setup [2020-11-22 Sun 22:35] Imagine you have weekly meetings with other people, and during the week you take notes about items to discuss at each meeting. When the time for a meeting comes, you want to quickly and easily search for all of the items to discuss at the meeting. @@ -7,7 +8,7 @@ You've been experimenting with different ways to track such data in Org. You've So you haven't decided on a long-term solution, but the meetings aren't going to wait--you need to search that data now, and you have a mix of both tags and properties in your entries. What you need is to be able to search for all of the entries about Alice (which you've tagged ~:personAlice:~) when you're meeting with her, and all of the entries about Bob (which have the property ~:person: Bob~) when you're meeting with him. What do you do? -* Contents +* Contents :noexport: :PROPERTIES: :TOC: :include siblings :ignore this :END: @@ -130,6 +131,8 @@ And that works fine. But it seems like a lot to type. Could we make the =perso (tags (concat "person" name))))) #+END_SRC +Now let's search again: + #+BEGIN_SRC elisp :results list :exports both :cache yes (org-ql-query :select '(org-get-heading :no-tags) :from (current-buffer) @@ -243,6 +246,8 @@ No problem, let's just select high-priority items: (org-ql-search (current-buffer) "person:Alice,Bob priority:A") #+END_SRC +Which shows: + #+BEGIN_EXAMPLE Query: (and (person "Alice" "Bob") (priority "A")) In:meetings.org [#A] Loud pet parakeet :personAlice: @@ -266,10 +271,14 @@ And, you know what, if you're just so busy that you don't even have time to type (tags (concat "person" name))))) #+END_SRC +Let's try it: + #+BEGIN_SRC elisp :results silent :exports code (org-ql-search (current-buffer) "p:Alice,Bob priority:A") #+END_SRC +And that shows: + #+BEGIN_EXAMPLE Query: (and (person "Alice" "Bob") (priority "A")) In:meetings.org [#A] Loud pet parakeet :personAlice: diff --git a/examples/export/setup/theme-darksun-local.setup b/examples/export/setup/theme-darksun-local.setup new file mode 100644 index 0000000..1ae015b --- /dev/null +++ b/examples/export/setup/theme-darksun-local.setup @@ -0,0 +1,17 @@ +# -*- mode: org; -*- + +# The files that hide sections and display a navbar and ToC are commented out. + +# #+HTML_HEAD: +#+HTML_HEAD: +# #+HTML_HEAD: + +#+HTML_HEAD: +#+HTML_HEAD: + +#+HTML_HEAD: +#+HTML_HEAD: +#+HTML_HEAD: +#+HTML_HEAD: +# #+HTML_HEAD: +#+HTML_HEAD: diff --git a/examples/export/setup/theme-darksun.setup b/examples/export/setup/theme-darksun.setup new file mode 100644 index 0000000..a5abe0e --- /dev/null +++ b/examples/export/setup/theme-darksun.setup @@ -0,0 +1,15 @@ +# -*- mode: org; -*- + +#+HTML_HEAD: +#+HTML_HEAD: +#+HTML_HEAD: + +#+HTML_HEAD: +#+HTML_HEAD: + +#+HTML_HEAD: +#+HTML_HEAD: +#+HTML_HEAD: +#+HTML_HEAD: +#+HTML_HEAD: +#+HTML_HEAD: diff --git a/examples/export/styles/darksun/css/darksun.css b/examples/export/styles/darksun/css/darksun.css new file mode 100644 index 0000000..212fb9a --- /dev/null +++ b/examples/export/styles/darksun/css/darksun.css @@ -0,0 +1,264 @@ +* { + border-color: #657b83; +} +a { + color: #268bd2 !important; +} +a:visited { + color: #6c71c4 !important; +} +body { + background-color: #002b36 !important; + color: #839496 !important; + font-family: Libre Baskerville, serif; +} +html { + background-color: #002b36 !important; +} +input, +textarea { + background-color: #073642 !important; + color: #839496 !important; +} +blockquote, +code, +pre { + background-color: #073642 !important; + border: 1px solid #094352 !important; + border-radius: 2px; + box-shadow: none; + color: #839496 !important; +} +code { + font-size: 1.2em; +} +img { + margin: 0.5em; +} +.buttons { + background-color: #073642 !important; + color: #93a1a1 !important; + font-family: sans-serif; + font-size: 1.5rem; +} +.hsReviewPanel { + display: none; +} +.title { + border: 0; +} +#postamble { + display: none; +} +#banner { + text-align: center; +} +#tabs { + background-color: #073642 !important; + float: left; + margin: 0px 0px 20px 0px; + padding: 0; + width: 100%; + font-family: sans-serif; +} +#tabs ul { + margin: 0; + padding: 0; +} +#tabs li { + float: left; + list-style-type: none; + margin: 0px 2px 0px 0px; + padding: 0px 0px 0px 0px; + white-space: nowrap; +} +#tabs li a { + display: block; + font-size: 120%; + font-weight: bold; + margin: 0; + padding: 4px 10px 4px 10px; + text-decoration: none; +} +#tabs li a:hover { + color: #90c5eb !important; +} +#tabs li.ui-tabs-active a { + background-color: #094352 !important; + color: #59a9e1 !important; +} +#content { + clear: both; + width: 80%; +} +#content > p { + margin: 1em; +} +#content > div > h2 { + margin: 0.25rem; +} +#postamble { + text-align: right; +} +blockquote { + background-color: #073642 !important; + padding-left: 0.75em; + padding-right: 0.75em; + padding-top: 0.01em; + padding-bottom: 0.01em; + overflow: hidden; + margin-left: 0; +} +.outline-text-2, +.outline-text-3, +.outline-text-4, +.outline-text-5, +.outline-text-6 { + padding-left: 4%; +} +.outline-3 { + padding-left: 4%; +} +.outline-4 { + padding-left: 4%; +} +.outline-5 { + padding-left: 4%; +} +.outline-6 { + padding-left: 4%; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: sans-serif; + border-bottom: 2px solid; + border-color: #094352 !important; +} +a:hover { + text-decoration: underline; +} +#search { + display: block; + float: right; +} +.tag { + background-color: #002b36 !important; + color: #93a1a1 !important; + border: 0px; +} +.tag span { + background-color: #094352 !important; + margin: 0.25em; + padding: 0.25em; +} +#table-of-contents { + display: table; + float: right; +} +#table-of-contents h2 { + display: none; +} +#table-of-contents li { + list-style-type: none; +} +#table-of-contents a:hover { + text-decoration: underline; +} +#minitoc { + background-color: #073642 !important; + position: fixed; + top: 0px; + right: -5px; + padding: 0.5em; + z-index: 200; + width: 18%; + word-wrap: normal; + overflow: hidden; +} +#minitoc ul { + margin: 0.5em; + padding-left: 1em; +} +#minitoc h2 { + margin-top: 0; + margin-bottom: 0; + font-size: 110%; +} +#minitoc a { + display: block; + font-family: sans-serif; + text-decoration: none; +} +#minitoc a:hover { + text-decoration: underline; +} +/* #minitoc a { */ +/* display: none; */ +/* } */ +#minitoc:hover a { + display: block; +} +#toTop { + top: -100px; + position: fixed; + text-align: center; + z-index: 999; + display: none; +} +#toTop:hover { + text-decoration: underline; +} +#left-panel-wrapper { + display: none; + position: fixed; + z-index: 200; +} +#left-panel-contents { + display: none; + position: fixed; + text-align: left; + top: 0; + z-index: 200; +} +#left-panel-button { + transform: rotate(90deg); + left: -23px; + position: fixed; + top: 50%; + z-index: 200; +} +#right-panel-wrapper { + position: fixed; + z-index: 200; +} +#right-panel-contents { + display: none; + z-index: 200; + position: fixed; + text-align: left; + top: 0; +} +.snippet-copy-to-clipboard { + display: none; + position: absolute; + left: 50px; + top: 50px; +} +.org-ol > li, +.org-dl > dt { + margin-top: 0.5em; +} +.org-ul { + margin-top: 1em; + margin-bottom: 1em; +} +.org-ul li { + margin-top: 0.5em; +} +.org-ul > li > a { + font-family: sans-serif; +} diff --git a/examples/export/styles/darksun/css/hideshow.css b/examples/export/styles/darksun/css/hideshow.css new file mode 100644 index 0000000..4ded9b5 --- /dev/null +++ b/examples/export/styles/darksun/css/hideshow.css @@ -0,0 +1,110 @@ +/* hideshow.css --- HideShow CSS file */ + +/* Copyright (C) 2014 All Right Reserved, Fabrice Niessen */ + +/* This file is free software: you can redistribute it and/or */ +/* modify it under the terms of the GNU General Public License as */ +/* published by the Free Software Foundation, either version 3 of */ +/* the License, or (at your option) any later version. */ + +/* This file is distributed in the hope that it will be useful, */ +/* but WITHOUT ANY WARRANTY; without even the implied warranty of */ +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ +/* GNU General Public License for more details. */ + +/* Author: Fabrice Niessen <(concat "fniessen" at-sign "pirilampo.org")> */ +/* URL: https://github.com/fniessen/hide-show/ */ +/* Version: 20140912.1722 */ + +.hsExpanded.hsAnchor {background: #EE7700;} + +.buttons { + padding: 0px 7px 13px 0px; + background: #0A3F69; +} + +.hsButton { + color: white; + float: right; + font-size: 70%; + margin-left: 10px; +} + + .hsButton:hover { + text-decoration: underline; + cursor: pointer; + } + +.ellipsis { + color: #999999; + /* background-color: #FFF8C0; */ + /* float: right; */ + margin-left: 0.6em; +} + +.hsReview { + border: 1px solid #A4A4A4; + background-color: white; + z-index: 500; /* must be greater then z-index of hsOverlay */ + position: relative; /* required for z-index to work */ +} + +#hsOverlay { + width: 100%; + height: 100%; + position: fixed; + left: 0px; + top: 0px; + background-color: #000; + opacity: .70; + z-index: 250; /* must be greater than any other z-index (except the one for .hsReview */ +} + +.hsReviewPanel { + background-color: #757176; + color: white; + line-height: 1.1em; + margin: 10px 0px; + padding: 10px; + position: fixed; + width: auto; + bottom: 0px; + right: 0px; + z-index: 501; + text-align: center; +} + + .hsReviewPanel.hsReviewing { + display: none; + } + + .hsReviewPanel:hover { + cursor: pointer; + } + +.hsReviewingPanel { + background-color: white; + color: #757176; + line-height: 1.1em; + margin: 10px 0px; + padding: 10px; + position: fixed; + width: auto; + bottom: 0px; + right: 0px; + z-index: 501; + text-align: center; +} + +.hsReviewButton:hover { + cursor: pointer; +} + +.hsUnselectable { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} diff --git a/examples/export/styles/darksun/css/htmlize.css b/examples/export/styles/darksun/css/htmlize.css new file mode 100644 index 0000000..f08e781 --- /dev/null +++ b/examples/export/styles/darksun/css/htmlize.css @@ -0,0 +1,145 @@ +.org-bold { /* bold */ font-weight: bold; } +.org-bold-italic { /* bold-italic */ font-weight: bold; font-style: italic; } +.org-buffer-menu-buffer { /* buffer-menu-buffer */ font-weight: bold; } +.org-builtin { /* font-lock-builtin-face */ color: #7a378b; } +.org-button { /* button */ text-decoration: underline; } +.org-calendar-today { /* calendar-today */ text-decoration: underline; } +.org-change-log-acknowledgement { /* change-log-acknowledgement */ color: #b22222; } +.org-change-log-conditionals { /* change-log-conditionals */ color: #a0522d; } +.org-change-log-date { /* change-log-date */ color: #8b2252; } +.org-change-log-email { /* change-log-email */ color: #a0522d; } +.org-change-log-file { /* change-log-file */ color: #0000ff; } +.org-change-log-function { /* change-log-function */ color: #a0522d; } +.org-change-log-list { /* change-log-list */ color: #a020f0; } +.org-change-log-name { /* change-log-name */ color: #008b8b; } +.org-comint-highlight-input { /* comint-highlight-input */ font-weight: bold; } +.org-comint-highlight-prompt { /* comint-highlight-prompt */ color: #00008b; } +.org-comment { /* font-lock-comment-face */ color: #b22222; } +.org-comment-delimiter { /* font-lock-comment-delimiter-face */ color: #b22222; } +.org-completions-annotations { /* completions-annotations */ font-style: italic; } +.org-completions-common-part { /* completions-common-part */ color: #000000; background-color: #ffffff; } +.org-completions-first-difference { /* completions-first-difference */ font-weight: bold; } +.org-constant { /* font-lock-constant-face */ color: #008b8b; } +.org-diary { /* diary */ color: #ff0000; } +.org-diff-context { /* diff-context */ color: #7f7f7f; } +.org-diff-file-header { /* diff-file-header */ background-color: #b3b3b3; font-weight: bold; } +.org-diff-function { /* diff-function */ background-color: #cccccc; } +.org-diff-header { /* diff-header */ background-color: #cccccc; } +.org-diff-hunk-header { /* diff-hunk-header */ background-color: #cccccc; } +.org-diff-index { /* diff-index */ background-color: #b3b3b3; font-weight: bold; } +.org-diff-nonexistent { /* diff-nonexistent */ background-color: #b3b3b3; font-weight: bold; } +.org-diff-refine-change { /* diff-refine-change */ background-color: #d9d9d9; } +.org-dired-directory { /* dired-directory */ color: #0000ff; } +.org-dired-flagged { /* dired-flagged */ color: #ff0000; font-weight: bold; } +.org-dired-header { /* dired-header */ color: #228b22; } +.org-dired-ignored { /* dired-ignored */ color: #7f7f7f; } +.org-dired-mark { /* dired-mark */ color: #008b8b; } +.org-dired-marked { /* dired-marked */ color: #ff0000; font-weight: bold; } +.org-dired-perm-write { /* dired-perm-write */ color: #b22222; } +.org-dired-symlink { /* dired-symlink */ color: #a020f0; } +.org-dired-warning { /* dired-warning */ color: #ff0000; font-weight: bold; } +.org-doc { /* font-lock-doc-face */ color: #8b2252; } +.org-escape-glyph { /* escape-glyph */ color: #a52a2a; } +.org-file-name-shadow { /* file-name-shadow */ color: #7f7f7f; } +.org-flyspell-duplicate { /* flyspell-duplicate */ color: #cdad00; font-weight: bold; text-decoration: underline; } +.org-flyspell-incorrect { /* flyspell-incorrect */ color: #ff4500; font-weight: bold; text-decoration: underline; } +.org-fringe { /* fringe */ background-color: #f2f2f2; } +.org-function-name { /* font-lock-function-name-face */ color: #0000ff; } +.org-header-line { /* header-line */ color: #333333; background-color: #e5e5e5; } +.org-help-argument-name { /* help-argument-name */ font-style: italic; } +.org-highlight { /* highlight */ background-color: #b4eeb4; } +.org-holiday { /* holiday */ background-color: #ffc0cb; } +.org-isearch { /* isearch */ color: #b0e2ff; background-color: #cd00cd; } +.org-isearch-fail { /* isearch-fail */ background-color: #ffc1c1; } +.org-italic { /* italic */ font-style: italic; } +.org-keyword { /* font-lock-keyword-face */ color: #a020f0; } +.org-lazy-highlight { /* lazy-highlight */ background-color: #afeeee; } +.org-link { /* link */ color: #0000ff; text-decoration: underline; } +.org-link-visited { /* link-visited */ color: #8b008b; text-decoration: underline; } +.org-log-edit-header { /* log-edit-header */ color: #a020f0; } +.org-log-edit-summary { /* log-edit-summary */ color: #0000ff; } +.org-log-edit-unknown-header { /* log-edit-unknown-header */ color: #b22222; } +.org-match { /* match */ background-color: #ffff00; } +.org-next-error { /* next-error */ background-color: #eedc82; } +.org-nobreak-space { /* nobreak-space */ color: #a52a2a; text-decoration: underline; } +.org-org-archived { /* org-archived */ color: #7f7f7f; } +.org-org-block { /* org-block */ color: #7f7f7f; } +.org-org-block-begin-line { /* org-block-begin-line */ color: #b22222; } +.org-org-block-end-line { /* org-block-end-line */ color: #b22222; } +.org-org-checkbox { /* org-checkbox */ font-weight: bold; } +.org-org-checkbox-statistics-done { /* org-checkbox-statistics-done */ color: #228b22; font-weight: bold; } +.org-org-checkbox-statistics-todo { /* org-checkbox-statistics-todo */ color: #ff0000; font-weight: bold; } +.org-org-clock-overlay { /* org-clock-overlay */ background-color: #ffff00; } +.org-org-code { /* org-code */ color: #7f7f7f; } +.org-org-column { /* org-column */ background-color: #e5e5e5; } +.org-org-column-title { /* org-column-title */ background-color: #e5e5e5; font-weight: bold; text-decoration: underline; } +.org-org-date { /* org-date */ color: #a020f0; text-decoration: underline; } +.org-org-document-info { /* org-document-info */ color: #191970; } +.org-org-document-info-keyword { /* org-document-info-keyword */ color: #7f7f7f; } +.org-org-document-title { /* org-document-title */ color: #191970; font-size: 144%; font-weight: bold; } +.org-org-done { /* org-done */ color: #228b22; font-weight: bold; } +.org-org-drawer { /* org-drawer */ color: #0000ff; } +.org-org-ellipsis { /* org-ellipsis */ color: #b8860b; text-decoration: underline; } +.org-org-footnote { /* org-footnote */ color: #a020f0; text-decoration: underline; } +.org-org-formula { /* org-formula */ color: #b22222; } +.org-org-headline-done { /* org-headline-done */ color: #bc8f8f; } +.org-org-hide { /* org-hide */ color: #ffffff; } +.org-org-latex-and-export-specials { /* org-latex-and-export-specials */ color: #8b4513; } +.org-org-level-1 { /* org-level-1 */ color: #0000ff; } +.org-org-level-2 { /* org-level-2 */ color: #a0522d; } +.org-org-level-3 { /* org-level-3 */ color: #a020f0; } +.org-org-level-4 { /* org-level-4 */ color: #b22222; } +.org-org-level-5 { /* org-level-5 */ color: #228b22; } +.org-org-level-6 { /* org-level-6 */ color: #008b8b; } +.org-org-level-7 { /* org-level-7 */ color: #7a378b; } +.org-org-level-8 { /* org-level-8 */ color: #8b2252; } +.org-org-link { /* org-link */ color: #0000ff; text-decoration: underline; } +.org-org-meta-line { /* org-meta-line */ color: #b22222; } +.org-org-mode-line-clock { /* org-mode-line-clock */ color: #000000; background-color: #bfbfbf; } +.org-org-mode-line-clock-overrun { /* org-mode-line-clock-overrun */ color: #000000; background-color: #ff0000; } +.org-org-quote { /* org-quote */ color: #7f7f7f; } +.org-org-scheduled { /* org-scheduled */ color: #006400; } +.org-org-scheduled-previously { /* org-scheduled-previously */ color: #b22222; } +.org-org-scheduled-today { /* org-scheduled-today */ color: #006400; } +.org-org-sexp-date { /* org-sexp-date */ color: #a020f0; } +.org-org-special-keyword { /* org-special-keyword */ color: #a020f0; } +.org-org-table { /* org-table */ color: #0000ff; } +.org-org-tag { /* org-tag */ font-weight: bold; } +.org-org-target { /* org-target */ text-decoration: underline; } +.org-org-time-grid { /* org-time-grid */ color: #b8860b; } +.org-org-todo { /* org-todo */ color: #ff0000; font-weight: bold; } +.org-org-upcoming-deadline { /* org-upcoming-deadline */ color: #b22222; } +.org-org-verbatim { /* org-verbatim */ color: #7f7f7f; } +.org-org-verse { /* org-verse */ color: #7f7f7f; } +.org-org-warning { /* org-warning */ color: #ff0000; font-weight: bold; } +.org-outline-1 { /* outline-1 */ color: #0000ff; } +.org-outline-2 { /* outline-2 */ color: #a0522d; } +.org-outline-3 { /* outline-3 */ color: #a020f0; } +.org-outline-4 { /* outline-4 */ color: #b22222; } +.org-outline-5 { /* outline-5 */ color: #228b22; } +.org-outline-6 { /* outline-6 */ color: #008b8b; } +.org-outline-7 { /* outline-7 */ color: #7a378b; } +.org-outline-8 { /* outline-8 */ color: #8b2252; } +.org-preprocessor { /* font-lock-preprocessor-face */ color: #7a378b; } +.org-query-replace { /* query-replace */ color: #b0e2ff; background-color: #cd00cd; } +.org-regexp-grouping-backslash { /* font-lock-regexp-grouping-backslash */ font-weight: bold; } +.org-regexp-grouping-construct { /* font-lock-regexp-grouping-construct */ font-weight: bold; } +.org-region { /* region */ background-color: #eedc82; } +.org-secondary-selection { /* secondary-selection */ background-color: #ffff00; } +.org-shadow { /* shadow */ color: #7f7f7f; } +.org-show-paren-match { /* show-paren-match */ background-color: #40e0d0; } +.org-show-paren-mismatch { /* show-paren-mismatch */ color: #ffffff; background-color: #a020f0; } +.org-string { /* font-lock-string-face */ color: #8b2252; } +.org-tool-bar { /* tool-bar */ color: #000000; background-color: #bfbfbf; } +.org-tooltip { /* tooltip */ color: #000000; background-color: #ffffe0; } +.org-trailing-whitespace { /* trailing-whitespace */ background-color: #ff0000; } +.org-type { /* font-lock-type-face */ color: #228b22; } +.org-underline { /* underline */ text-decoration: underline; } +.org-variable-name { /* font-lock-variable-name-face */ color: #a0522d; } +.org-warning { /* font-lock-warning-face */ color: #ff0000; font-weight: bold; } +.org-widget-button { /* widget-button */ font-weight: bold; } +.org-widget-button-pressed { /* widget-button-pressed */ color: #ff0000; } +.org-widget-documentation { /* widget-documentation */ color: #006400; } +.org-widget-field { /* widget-field */ background-color: #d9d9d9; } +.org-widget-inactive { /* widget-inactive */ color: #7f7f7f; } +.org-widget-single-line-field { /* widget-single-line-field */ background-color: #d9d9d9; } diff --git a/examples/export/styles/darksun/images/checked.png b/examples/export/styles/darksun/images/checked.png new file mode 100644 index 0000000000000000000000000000000000000000..ebdc03bdaba59b81824d0b0f2ac115762310d2dc GIT binary patch literal 3136 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4rT@h1`S>QUZZHe6Op)kO($dOzyVkXC&z|p}KK*`tC;#o{@B5y=+50|9oMF;-`RUWyycs8) z68_MA~3v6|4`oZ7f%q z%lFUTbMvL2Oo6i;1fw@u&eRbU@3K&ho}jnwe8&u_={ji=s%iUPG$>6IFFvFsUHdIE zCEajyaAl5Q_xU;J{8GDjdLN12+#(~WT&JXW>QT`q*BbXDQJRNB)^*(PN)EO>F0etL@s=d_*GJDZOwLf z_T;&T1UY(`XRb+6o9|oh?d_ZXsfaOR&*$`$9ogREqBoD-Id|;XjYCKNtakc){p^0t z!hG?Up%C22GWp;g>-~L0M{ohy1o+itWGIs6iT^z!`lGCRvWSRG$6)m`2QKI+W-t?n<+h5P_ zVqKx7?}Qf|o58TsPRpV0Eu#jPpu$D=BeDM%=hyu(5YVVN#Ft zSocECgv-})zw`S<7PW&R5vJw$~1KTIt3FbwLuqU7treyR74o7;rUC$e14ix#}l zNS?xcN?NolX`xZT>ww#Vp(`G*2n!K^WtJsk+Wf0UZSmBLZ5h>D_^p`cHk@-4UwHbW z`$gRs+AkcxSp4E;YhIXGVz^Br-$zA_b2Hm!+0F8odvO-wlQK2(=~zD9lQsO zHZtW1ZWF3zs%ErqkUhTds9<511@|0vJ+9-2MIW3h&I?CX8w>< zqqk2azWx1y?+=fE{LGTsq<3(lfRl$(j!KBYERJvve$MqBE{oVM_DQ%r5i%Fh?ojW% z-m&;dxr*c@#*@7wzCx4ACiG3zoAhpC+{BY9vYU)8yL1%ZD_ozfKUseAeXjr&AC(%F z8$z#DSE)v+vZ=&*JW}1N8mkiS{mgTk_q=IuCeN96$or+oPEW(7*E~bL=T4NGHf!oK zkJ;Yr9?QM*r{_)Br)R6#JoVzlj}sD4i=2{~xKqd2%~4~Ph&fmHrp_SuolcU=lUD2s zkvSt+ZRKoLX|>tP(Yn=sWi4$NN;2%Wh(u|=70K=R7U$=B&b3c@yYD*B zJcm5LIL~{L^OWOt?(5iV{Eur~_h8|KH5Q611>NWMW*5y?uq3KsuuG+489=iOBMwa*0 z9a+^^^j6p|K7A?nh2|HoD)Cyk*vPxlu`B8#_J-HSIfpBUEOuBN`FNt5vy_u z4lZ`{_B|c19ltv8^**z>*$d~c*z3PLy4ue@Ts(85)xA>j2?WTE? z_IvBhd8!uFGyCYwzbUlz42aM^C3-M;oSBWEw2adoDx!F0p;w77(OiT`%DHnN^x zbn8){>di}+i}t4Oe(J65tlf8&arN$NBH8t?b+666CZC&=`@_uFEO=YgwvF3Nw;kru zQl7AJZsA$J+~--_@4Z`d*ScD|y78~(@B1=6vXf+`WYng=(0iz}GTP|biiamkY~EOG znzGNN?B&9x%6_l>e3nNoS3fuFT+Z{w=Y;is>3Qm@$HeWJwewSKbjs?JqJRqYOkZ(r{{wSBYr(cw=kzt4A zTv=3k=ft;xH&$#|u{dzEwr$wESWCHF*;o$8Kpn2d6e}iEh2AC?V9H~>EGnOi5#C8d~9Y%d5?bge4hfJ8*>e&zL*j* z&*E(95i>o%eVdEc;<0!;_}kO(!2MK_N}#FRk7vCi{r_Al9i7)_m^M2eeJrr zhWQk;L%oNNtrWhfo>%+)Uu^Z{!pjfSqKZ#?76es%y7_(Meg3(hY_C?%D%|yw{jGdf zl!ornwE@|AuYIDEZhKUxRR5ounr>w+V-;0r^7G@3$gi8PK2JG6b)NbheVhFSiti7- zIr^ph@^m4+-qK?wb~|JWfBJ<7gufTPuYG-8Rovb?Z!gt8ob`C?=Id|Q&)<1)$HlVC z&(gB{OZ`v#U$fu(<<^&N-)?@}dVkaIs~7h#`TwWk@`0td-)&!i@8izm;)0(E{{voJ zxcl(M!->1!?4JBR=-u8u^Goah9c*iV(SEdliQH8Cu)3JvoBy3Zt8AwH-0iM=^}IC| zpAvdZzx|nhCf=?`+)mYAcYeU^74x^uu(5t>S^E0ssr1L^FV8>kFaB5SZ}wU4z2f$N zp8bhF?{nnL5AT1cTkmh*A5>BLX6x10=WZUjc|1Sp{+aWl`$H?L|HwX${r>yIcj3=B zo71dC?tCX`7$t6sWLD$G&3;#{LjG9 z@REU{)PRBERRRNp)eHs(@%%~gN8K107!m?}LR=Xb75cf>Hq)#zf>=0$-uxMTN30K%wRhIF8AUe^8&Tcs@nCZ2K@TA{ -^9N* zU7@RT1Wj2P7#J9nyxm>oS$PHaGB7Z37I;J!Gca%qgD@k*tT_@43=DmqE{-7*ms9(1 z3pFe7xG+!Ly*6U)rKlhO&zrEw$nWAmGlz9jEW@M;GmO0$eVZ>Gvr6v`W8VCI;a!&Z zs~7*s$=|!^zg3Kl-Mpv<6LJ5IXY@HU^7T%4O}P6*&N_d$_XPXm&fs(f=IcwR{K(}I z%Y3nK>Z(%-DY2*diWh7(m9RV`)3aE9|Kq-Y;p?vYue@&0=)ILwxKZ%%R0akH)e_f; zl9a@fRIB8o)Wnih1|tJQBV7YST|?s#BO@y#GbIKU^w z_5c6>uCA`9P9FRBZOZ>oDgWM@Gcz+h-pHt~#Q6UO`-&+{|E@B)STOWgF{DK@w6wJR z|NlSrVR!}u0|R48kY6x^!?PP{AWo9Ey9+}HtE>kD1EZs-i(`nz>9tc{PCKl?(b_B; zbK=v#{M+0aeKU>i)2gbZg;_pzdT-8E`_;8gbI}*BgTH@;%v8Tr_;YIUGTp#B50(F+ zXJRrwtvQ}!V;vfrT+a zv&M`wHq6r&aGNN5Oi@*)z4ZHGqf?zu{;k`?j+?r6mVB0(ap^1b4ZkZ_p33m;*m7Oa z!r;P7hlHyl4#yAsiMvQ-tXasm(`%#o6+J^2Tbl&VvS8PW6Af_*AKsYEyuIm`!K|dZ z8IctNE5v%m4zGImn<<4^%QAnLZMKxEmJA>3{}~RoD;&0IzmeYaX`aKHg=PAN4ZGGi z?mhRXX}-L)e!=9{?+i)P4Q~gZ;@#u@J@#FBod|~#AIs8g*19E&QvZGD*m@~{&o}wF z${M+S-?mmV|9&Jl!B@raBm)D3YKdz^NlIc#s#S7PYGO$$gOP!uiLQa6u90Pkp{bRD zv6ZR0wt=CQfx)tgIq4`Ga`RI%(<)Ikm{^&ZSs55YG?dTkJI27kz~JfX=d#Wzp$PyX C+w#5u literal 0 HcmV?d00001 diff --git a/examples/export/styles/darksun/images/halfchecked.png b/examples/export/styles/darksun/images/halfchecked.png new file mode 100644 index 0000000000000000000000000000000000000000..afd695e4145e06f388b207903aa87c474f47e957 GIT binary patch literal 3080 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4rT@h1`S>QUZZHe6Op)kO($dOzyVkXC&z|p}KK*`tC;#o{@B5y=+50|9oMF;-`RUWyycs8) z68_MA~3v6|4`oZ7f%q z%lFUTbMvL2Oo6i;1fw@u&eRbU@3K&ho}jnwe8&u_={ji=s%iUPG$>6IFFvFsUHdIE zCEajyaAl5Q_xU;J{8GDjdLN12+#(~WT&JXW>QT`q*BbXDQJRNB)^*(PN)EO>F0etL@s=d_*GJDZOwLf z_T;&T1UY(`XRb+6o9|oh?d_ZXsfaOR&*$`$9ogREqBoD-Id|;XjYCKNtakc){p^0t z!hG?Up%C22GWp;g>-~L0M{ohy1o+itWGIs6iT^z!`lGCRvWSRG$6)m`2QKI+W-t?n<+h5P_ zVqKx7?}Qf|o58TsPRpV0Eu#jPpu$D=BeDM%=hyu(5YVVN#Ft zSocECgv-})zw`S<7PW&R5vJw$~1KTIt3FbwLuqU7treyR74o7;rUC$e14ix#}l zNS?xcN?NolX`xZT>ww#Vp(`G*2n!K^WtJsk+Wf0UZSmBLZ5h>D_^p`cHk@-4UwHbW z`$gRs+AkcxSp4E;YhIXGVz^Br-$zA_b2Hm!+0F8odvO-wlQK2(=~zD9lQsO zHZtW1ZWF3zs%ErqkUhTds9<511@|0vJ+9-2MIW3h&I?CX8w>< zqqk2azWx1y?+=fE{LGTsq<3(lfRl$(j!KBYERJvve$MqBE{oVM_DQ%r5i%Fh?ojW% z-m&;dxr*c@#*@7wzCx4ACiG3zoAhpC+{BY9vYU)8yL1%ZD_ozfKUseAeXjr&AC(%F z8$z#DSE)v+vZ=&*JW}1N8mkiS{mgTk_q=IuCeN96$or+oPEW(7*E~bL=T4NGHf!oK zkJ;Yr9?QM*r{_)Br)R6#JoVzlj}sD4i=2{~xKqd2%~4~Ph&fmHrp_SuolcU=lUD2s zkvSt+ZRKoLX|>tP(Yn=sWi4$NN;2%Wh(u|=70K=R7U$=B&b3c@yYD*B zJcm5LIL~{L^OWOt?(5iV{Eur~_h8|KH5Q611>NWMW*5y?uq3KsuuG+489=iOBMwa*0 z9a+^^^j6p|K7A?nh2|HoD)Cyk*vPxlu`B8#_J-HSIfpBUEOuBN`FNt5vy_u z4lZ`{_B|c19ltv8^**z>*$d~c*z3PLy4ue@Ts(85)xA>j2?WTE? z_IvBhd8!uFGyCYwzbUlz42aM^C3-M;oSBWEw2adoDx!F0p;w77(OiT`%DHnN^x zbn8){>di}+i}t4Oe(J65tlf8&arN$NBH8t?b+666CZC&=`@_uFEO=YgwvF3Nw;kru zQl7AJZsA$J+~--_@4Z`d*ScD|y78~(@B1=6vXf+`WYng=(0iz}GTP|biiamkY~EOG znzGNN?B&9x%6_l>e3nNoS3fuFT+Z{w=Y;is>3Qm@$HeWJwewSKbjs?JqJRqYOkZ(r{{wSBYr(cw=kzt4A zTv=3k=ft;xH&$#|u{dzEwr$wESWCHF*;o$8Kpn2d6e}iEh2AC?V9H~>EGnOi5#C8d~9Y%d5?bge4hfJ8*>e&zL*j* z&*E(95i>o%eVdEc;<0!;_}kO(!2MK_N}#FRk7vCi{r_Al9i7)_m^M2eeJrr zhWQk;L%oNNtrWhfo>%+)Uu^Z{!pjfSqKZ#?76es%y7_(Meg3(hY_C?%D%|yw{jGdf zl!ornwE@|AuYIDEZhKUxRR5ounr>w+V-;0r^7G@3$gi8PK2JG6b)NbheVhFSiti7- zIr^ph@^m4+-qK?wb~|JWfBJ<7gufTPuYG-8Rovb?Z!gt8ob`C?=Id|Q&)<1)$HlVC z&(gB{OZ`v#U$fu(<<^&N-)?@}dVkaIs~7h#`TwWk@`0td-)&!i@8izm;)0(E{{voJ zxcl(M!->1!?4JBR=-u8u^Goah9c*iV(SEdliQH8Cu)3JvoBy3Zt8AwH-0iM=^}IC| zpAvdZzx|nhCf=?`+)mYAcYeU^74x^uu(5t>S^E0ssr1L^FV8>kFaB5SZ}wU4z2f$N zp8bhF?{nnL5AT1cTkmh*A5>BLX6x10=WZUjc|1Sp{+aWl`$H?L|HwX${r>yIcj3=B zo71dC?tCX`7$t6sWLD$G&3;#{LjG9 z@REU{)PRBERRRNp)eHs(@%%~gN8K1082kc!LR=Xb7(}%_RK4pA5*D#?3u%W=Fi2RW z>Rl(Q?IECGrX4y#J9L7Sp|6~gi=2@QE4Pqa>3pM*A^`=nsOe{IQad4v4H6bbO+Wkp z|Nm?15g7~&4B{n0e!&b5OP>j>`8LB->FhC8A4~3jAEo^bKW5#x(s{tZz`&T~?d~G^ zCU0v80|NtRfk$L90|U1(2s1Lwnj^u$z>w$Z;us=vIki8W?|=f2v+s@8cVb2VCr{%m zeYoG|WFM~x=OjySA&aG3HlC|a?+rVCx8+8e!}Vhq-ng{aCCIryTgbob$Jv&dm60>j zeO5C>+Awq~+?BTId~^T6bE%MN`74&*k5+z_RQk0#X4kR5&+?a6b1jOTf8!ei1A}Ub zYeY#(Vo9o1a#3nxNh*VpfuWJEfuXLUafp$Tm64g1sin4op_PHb9L2vPC>nC}Q!>*k Xacg*6(QWTKjuBBy{?hvLGUqJakmU0qqEPb}zQ?(1vl5e?Mg>EgP<{zP$6U-ROQ zu2a7sRxi%Jw&(k^v;VeUw|lQz`~GlIyvbvLx3(rf_nY4%M1;3f6T0AWQd-kD9X5C zB7=f(YKIF$hBd>Q^I=`C3=swlB5B@7EE#U-G8lMT#JFBV&*2Bohp{DjnN#sh3w(sdU6`)JjJy| z%o3RrXF2E{s+=il)M�PV*|l=S%D z)AOqf92giLmegJRr=!1Fs==DAA@|=R)pslzaoh_^zjvo?;&<4}u;Asb@XrOHuyI{D zVdu@8PoF%w;dR6*I`LF^?Z5Ox@dv{H=KQR)`SbnjgME)w8jWHb-C2G-ICSQV&r+H5 zlQ>(F?mX*v|FfO@{~y`QN=H1Cc1CE4JPs5p>~opb^Vw*rQm4}-&7-Q9{-->;uk)MP z?!GC*hAcbA>WR85Vw0J_d8-RVlpJ{Uoq^%g?)-ySG&qj}HTE=evN)dL5KdIQ z)xnt56`|b6AwIM9hvGpG7NKqxg_9j3flA&JO+5^QxN*)_l1-a!&&`e3ua#^+9GA!W47@5h1D7SxBBgz zj~g6#A;E5ZB*#GS*kPxQ7CM}hlTU9{TEnyYnAC=_HO#v^zb2pGSk5BU=z4(DjHTR3 zAyLlJBf=-b?}o<;1sN3+!CRc>oXjnOht!oOukhZYwoCBm5s4%t!}bUd9p!66{lfYJ z)g8`9%vFRsosHCYPGIufxFqC~&?UD^c|poAbym(c@sLuUeDYRG_9k}ADLM+;Ct9D( zeRBAT@e}DMil3sn<|a85F8LAsNF(S}mY3vHfu|-<*-~d1IfgEt6?AvS!Vp2z{wSxl z3%3QW4{To{Y*ISQ@@%u1r}xsei>6+d$`H@^p1J--$u7aWQ|0`oUkJZ+`lb7e=`WbS zl=JYmi8deaNY{{@Ex|64E?F+IeumF7$;;i6$3qOv4PVcAK9hfDerSZ&Hmw@biKn=x zdQDZEDy^j-vU1h3RrgkLhkgz{9n!xtF1U7`Tc~mHe>d_u9`{m@QFl-8KgT-vdGO8PyUV$j zi}~7GeO;%wDtoE)!rd*_s-Zv4yNvdDMa_T;rjSw=362#t!}?6!99TGea2*X~{}xO}JIWWUw(uB9K| z@NwJ8Z7a7+Zg_fw?e@BDx!Y zH{DKew)C;PC-y$^D*0L%`>^WL*SWJd&pz~a#qIU)%-&_bYrix9jLCe>d6CA&7Y?6r zeh~QdqCNNgsRr5i*VOqt`!}Xte?weiF{Ks~WZOrc( z-HVJrV}4-!iS4KT59iN|Z@uqaulzsdf7Sns49go-8Pl0N8yy>?nSTGMs!`nEXK!^o z_1mVKZykFU>~s`$>~C7z^z`6b#f%egCahKb{(Q!{iRUu;S_A4{T)V)Y_$;yea2=0& zYkO;Wn|)tFM}*Wx!|A5w7w~uqa%k{cmrdoEZ^s;PjneQ^$Wq(XB zE%MCOh`AA-Io>jsdRptu@^bdw|MT{9`=hy!xF1)G&(J>+TTr?1+@UY6%f(je3F%l( zbMWc#vzYS1Q9x^jSdZ!_;d0eJuF6BQhtiewFPdHKxZxb5CFl3aL(8{n(wm8UOe9?^Mp05k-A*loR+-^iPE^P#l9&$H9T$q+0Ijz(^k*ZKKu5}=2N#P zn9q4X|NmQ|LqZQHZCl#r9j+@I@oev!EXz$cg_XCt9*eSSw}r8-vDwkJO)K}R|LV8l z)7RDQRjcyNZj3v4s_EcX#_ZSJpLO=5U*2G_JbEWM>JG<72+!miM zT7N_1PQ_E{_WV=(XWp5zW##H4J5$c5T}pfT?4IttXufHyrazm`m&bea))wCTVO!pQ z{pNaGfBIRC+q;{$IqzJ&-)6_2)a577owc?PSrYOkt)mB z$?2cY_spF-x3*62=bePxi?&riPd;})i$DMUE`gGVmt6mMKeygveJhIR>@!u2w#o?{vP3ODqSM8(!-MR7pPJPya z@&%tS-f{oV*4ljj-^8!|{LZZXGy6}s@wUC5XJKb)`N+QL@5?{SozJJ7SDPpPZ_&Tg z>%_(M>wa8)ef`|U85b8$|1^E_{@j|Se_tJVExG>ou|vn?_MNOSHjMic_WSL@@;l|S z?-$?a_kU8OP_OYnpUa*cC(i%XUQr$#ILlog@+6MtPWTPM=!6zbaLrFCURZJ|q?SLF(= zt%0k>R0EZlwib0hFYRW8~)FvFBeNUU&WVQ8T}Ii%W0J?{6=SUE8UKDBTS50j zCVfWT)zj}s?|=3=enDcPtm#P=FU><5&#LVF@4vWlS$?kl|DfYf-|=kV|Dh^y$7|5>He3OxV zy}tS#W7)ac%$s}SbL$FxPn*lvvaqCV5^TOXL4hq<_n25%u8!&^vqKR9LhsgwW%DtNi*8|N6H5uX)2{AEnW-*WY%>vZvDhxeBk3f9Yv`q8w~u#G6pFqJQB#Kb?T< z1@c?Jo;GOceDG$2|8~B}jW<$X|KfEywxy37^AhGOlg09S-lIHz!b9A1uO1v?N z_qnuVi|@k}wez>w_nm5=`*!!*tn=>wZ1Xv8-|?qr zW?{sqslG=mjmtZ9nLgKe$KOk=JZz-*^;-_xJze$>KRzv;|MXCVLxL8M(d8W*r`i91 zBKc}{+Ozk<^)GZAxA~m*|9H&lxrc*uE%WrMYPtDL22+{OzI;0F)GPKt5?upkX2QGOryi>o~aWNv< z>F^reCK1sp?R!gR{J$WmAnSRJ>(ur~A`J(QFOuOD^V!p&w)D`wwG1Ar0evbO6*VhmI^An$5I%>=?=f}(KJD#7Lm3ck8vrIKFYO;rBlK$xvJB=CEpS#M> za_z|Os#|?q*4^1AyV4|xi%DONPj}gx3pUjy74LecGikD(65QeGscu@LUkVU3T;r!EwzFl2+ z#8>N1i;L&s*2V3TcDJ|YuRB(9ZT7_r|H9)WZ0_z`|MvjnqB|_1rd_wJ+HYC4&$4QN z<;oYjbymsRWvjx1_pbO*>$Pl^o8VJ5i@(oi^UTY;Z@%UKRB^l9aGm!0&!6YJ-mZT! zUqbF)WqGi0@9XQ6y_S11rM+4ss&IN$(2VoN8{b+m%gX+kd2erW{@eeqg$q9}XJj~V zBl`D$4dIoK?=4H;f4Tb8k?XIfnTF}-#kifFq-t?eB`DiRM6gQk=0ZicYQ5sOMdwpy z?EG}^$VBFR3H$m=$MVCGZHzSucYgeQnEn3WyYC0AC;Psh)g~2Y^3t{3gyZd!iKh-8 zS=f6h#>DWN_?Or>CrbaUW}d;|*6?wWobmgsZ_4`q^DgTe~ HDWM4fo4`&G literal 0 HcmV?d00001 diff --git a/examples/export/styles/darksun/images/note.png b/examples/export/styles/darksun/images/note.png new file mode 100644 index 0000000000000000000000000000000000000000..1c6b8eb0fb39720842be86b0343adfdcbf929982 GIT binary patch literal 4567 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4mJh`hT^KKFANL}oCO|{#S9GG!XV7ZFl&wk z0|S?Trn7TEKt_H^esM;Afr6*AvqC{pep+TuDg#5st+~PJA;B-jY`@?8;^f`YFvUAR zNR&e%$f>(QWTKjuBBy{?hvLGUqJakmU0qqEPb}zQ?(1vl5e?Mg>EgP<{zP$6U-ROQ zu2a7sRxi%Jw&(k^v;VeUw|lQz`~GlIyvbvLx3(rf_nY4%M1;3f6T0AWQd-kD9X5C zB7=f(YKIF$hBd>Q^I=`C3=swlB5B@7EE#U-G8lMT#JFBV&*2Bohp{DjnN#sh3w(sdU6`)JjJy| z%o3RrXF2E{s+=il)M�PV*|l=S%D z)AOqf92giLmegJRr=!1Fs==DAA@|=R)pslzaoh_^zjvo?;&<4}u;Asb@XrOHuyI{D zVdu@8PoF%w;dR6*I`LF^?Z5Ox@dv{H=KQR)`SbnjgME)w8jWHb-C2G-ICSQV&r+H5 zlQ>(F?mX*v|FfO@{~y`QN=H1Cc1CE4JPs5p>~opb^Vw*rQm4}-&7-Q9{-->;uk)MP z?!GC*hAcbA>WR85Vw0J_d8-RVlpJ{Uoq^%g?)-ySG&qj}HTE=evN)dL5KdIQ z)xnt56`|b6AwIM9hvGpG7NKqxg_9j3flA&JO+5^QxN*)_l1-a!&&`e3ua#^+9GA!W47@5h1D7SxBBgz zj~g6#A;E5ZB*#GS*kPxQ7CM}hlTU9{TEnyYnAC=_HO#v^zb2pGSk5BU=z4(DjHTR3 zAyLlJBf=-b?}o<;1sN3+!CRc>oXjnOht!oOukhZYwoCBm5s4%t!}bUd9p!66{lfYJ z)g8`9%vFRsosHCYPGIufxFqC~&?UD^c|poAbym(c@sLuUeDYRG_9k}ADLM+;Ct9D( zeRBAT@e}DMil3sn<|a85F8LAsNF(S}mY3vHfu|-<*-~d1IfgEt6?AvS!Vp2z{wSxl z3%3QW4{To{Y*ISQ@@%u1r}xsei>6+d$`H@^p1J--$u7aWQ|0`oUkJZ+`lb7e=`WbS zl=JYmi8deaNY{{@Ex|64E?F+IeumF7$;;i6$3qOv4PVcAK9hfDerSZ&Hmw@biKn=x zdQDZEDy^j-vU1h3RrgkLhkgz{9n!xtF1U7`Tc~mHe>d_u9`{m@QFl-8KgT-vdGO8PyUV$j zi}~7GeO;%wDtoE)!rd*_s-Zv4yNvdDMa_T;rjSw=362#t!}?6!99TGea2*X~{}xO}JIWWUw(uB9K| z@NwJ8Z7a7+Zg_fw?e@BDx!Y zH{DKew)C;PC-y$^D*0L%`>^WL*SWJd&pz~a#qIU)%-&_bYrix9jLCe>d6CA&7Y?6r zeh~QdqCNNgsRr5i*VOqt`!}Xte?weiF{Ks~WZOrc( z-HVJrV}4-!iS4KT59iN|Z@uqaulzsdf7Sns49go-8Pl0N8yy>?nSTGMs!`nEXK!^o z_1mVKZykFU>~s`$>~C7z^z`6b#f%egCahKb{(Q!{iRUu;S_A4{T)V)Y_$;yea2=0& zYkO;Wn|)tFM}*Wx!|A5w7w~uqa%k{cmrdoEZ^s;PjneQ^$Wq(XB zE%MCOh`AA-Io>jsdRptu@^bdw|MT{9`=hy!xF1)G&(J>+TTr?1+@UY6%f(je3F%l( zbMWc#vzYS1Q9x^jSdZ!_;d0eJuF6BQhtiewFPdHKxZxb5CFl3aL(8{n(wm8UOe9?^Mp05k-A*loR+-^iPE^P#l9&$H9T$q+0Ijz(^k*ZKKu5}=2N#P zn9q4X|NmQ|LqZQHZCl#r9j+@I@oev!EXz$cg_XCt9*eSSw}r8-vDwkJO)K}R|LV8l z)7RDQRjcyNZj3v4s_EcX#_ZSJpLO=5U*2G_JbEWM>JG<72+!miM zT7N_1PQ_E{_WV=(XWp5zW##H4J5$c5T}pfT?4IttXufHyrazm`m&bea))wCTVO!pQ z{pNaGfBIRC+q;{$IqzJ&-)6_2)a577owc?PSrYOkt)mB z$?2cY_spF-x3*62=bePxi?&riPd;})i$DMUE`gGVmt6mMKeygveJhIR>@!u2w#o?{vP3ODqSM8(!-MR7pPJPya z@&%tS-f{oV*4ljj-^8!|{LZZXGy6}s@wUC5XJKb)`N+QL@5?{SozJJ7SDPpPZ_&Tg z>%_(M>wa8)ef`|U85b8$|1^E_{@j|Se_tJVExG>ou|vn?_MNOSHjMic_WSL@@;l|S z?-$?a_kU8OP_OYn#xsS_07HEd1se~;*GE-sSM9cL06WnlyM9ZZEOix!gWMC%X=x;%B~5k#6mjN9h{mD zNjpvSa9OfVKtRzgFtfnpl11<5mp^Xz?cEpm{@&j2=Xm&cmu>3wowF}xN%8wTGmFpv zIX|=b+#JDU5^`yiZJwkt|9QLo?dqhu`n=PpdA}VxD7AXS$;pYg1W$GKEd6xr?Aeu1 zu0|KWGOy0E`}s91#rE@J>Ftbv_Lo&`Q@{Hvtv}DN=Y7@6;1j>5E%!``+Hl7t=vIcP zkofbx2jnwnN!spBuCV#Jn%nWSz1yK%_n1rn?hK7gSbA;xYl}}whkxFCc5bhU?Jurr zj~Ur`o^C&wP_Wl+&#tmpxAz@6mf=_y_wV)P$>IIy|6hJ@@hj!msek8L9+d1`TmODp z%J*Zr`N`^82P8rY`9#kwJj~kDWnjg|>=ebJ-Z*uI<1Wov*55B(*w*~zru~ zHU^b!xwc{9&SflG*F@CP1&ybBnv`9-w6i_^>_%PrI<0A2TlQ3+FW+8$S9ktv)pNa* z1lb;l1n*B{Eh?`k(cde-Ga!O#<%3O*|zdgFVotY+%m zwVQV9UoT8?WoS>Yz7&7*&WqglztjrrnhsrIaJw6KVFknD=A|3g?%wAcYQ%d$e7<$Z ztNh)aOo^JKOf!mQ3?YFFrkT-NL2y;+$b& zI9G9QYvhy5-8HJGb$h-lmriV(CUxu_uRei_;a_1O?f4!L*A#L%sY2BZqRJvJpEYFYL2UJPeAN-KaeJd zfc&TNE7sk1F!-DqdEk-UG?Qg(LOnMJ#rw{WV+xS?zvEAQWVLWppMe#N@YIGE9fz)1 z2F=`hCHkkh)}ame&Xw+1*FA5(#_@{OoMfJWzDn8T5*LHYwiOQ^s~8`7T>C}5@t8ou&P^#gTTdk^$t--49>(er!E}0RM%NLySzHX3p<5U9uG$*Qz1Um*PL`JH z<9L&MoJd=HG9a^`*1k&Q~rSBYscoLO<@s{z-p$#I+wZU?p22t0Q`!S_e3(4Jw_ zmK{%{T3TmFPTdv9so*Dj^7)^4yWB5DsJ6Rr?$N*VvZ3zG&urGD)QtGYmoCnVsC=K> z8JembJG(~jxX+z}B})81v_4*QD&t#nz|Sgzll!Gdi)z@C1y$Dft3KQB`LlWTnw$Te zn{McwR+sIZx?#g)K97S=Wqk3f_f|0!ELdl6Gxc^sQq`5?UKTez#ZPaYu+7C|qfY7D ze|DiKr5JXV)%W`(-?p{6G%aFX=%ncdGvq#7PQ6|5$RW1A;(Vfs%(Zv3CBJu0J@_d` z=e+AKvqRGqj@nMy|Es#>39~`@OWQ|>GLG)qpn7TR^pvBg42+){%UpXGy-mR~;QD;) zm*p~F*^X6NZ`-b@(>Xy%wB?0IV#!i_mdo;nC2wYOxvws;I)2D&#|=&K%;*?<^CO=1 zmoM(-zS`yd_2ieSf7RWj&-3v6B&ZATn5p7t6=}3ES$T<0>lH5*)oIDe>alZ5gv=)& z@l%tKoAKaC#^ae!xvo@uZOXkDaQ@@Lug~XCRgS*3>zZP$wCIf^jxBu2C2I_#rf=F5 zd1j-+5|>+Im$q&c<_kOI8Zl|%f4<@xgTE)eT3>E5UmVW=wY1h?f>rg(_gsN13Rc+) zeZ3lTb@|FwzQLiB53T)HVC;D(tAEw3kjy=HzKixrEx)DACEEOd*CBT1m7CYgy?R}B zVN=Y$f0OsE-}~=WaD7eQyGQ4aePg?3cdzGdXyf)7Y`n)B1g6M8eG<|mx!d5>m)__a zgMI&wSihfFvvJLest4UCLCm+)x87T9|Kr^)^YR$Enl-7t%0|ADd!qPnZ=E>JMcY^A z*ptnbe>~@3>^w1NR*uAbS$%eSc%Cx8<4K zz8-$wZqk;ik2zG8l4m;QxSBt|yR!KDoc?~gTe~DWM4f#tOa5 literal 0 HcmV?d00001 diff --git a/examples/export/styles/darksun/images/tip.png b/examples/export/styles/darksun/images/tip.png new file mode 100644 index 0000000000000000000000000000000000000000..743ef89c3a48cb65d51f0efb7305be9f2beba401 GIT binary patch literal 4447 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4mJh`hT^KKFANL}oCO|{#S9GG!XV7ZFl&wk z0|S?Trn7TEKt_H^esM;Afr6*AvqC{pep+TuDg#5st+~PJA;B-jY`@?8;^f`YFvUAR zNR&e%$f>(QWTKjuBBy{?hvLGUqJakmU0qqEPb}zQ?(1vl5e?Mg>EgP<{zP$6U-ROQ zu2a7sRxi%Jw&(k^v;VeUw|lQz`~GlIyvbvLx3(rf_nY4%M1;3f6T0AWQd-kD9X5C zB7=f(YKIF$hBd>Q^I=`C3=swlB5B@7EE#U-G8lMT#JFBV&*2Bohp{DjnN#sh3w(sdU6`)JjJy| z%o3RrXF2E{s+=il)M�PV*|l=S%D z)AOqf92giLmegJRr=!1Fs==DAA@|=R)pslzaoh_^zjvo?;&<4}u;Asb@XrOHuyI{D zVdu@8PoF%w;dR6*I`LF^?Z5Ox@dv{H=KQR)`SbnjgME)w8jWHb-C2G-ICSQV&r+H5 zlQ>(F?mX*v|FfO@{~y`QN=H1Cc1CE4JPs5p>~opb^Vw*rQm4}-&7-Q9{-->;uk)MP z?!GC*hAcbA>WR85Vw0J_d8-RVlpJ{Uoq^%g?)-ySG&qj}HTE=evN)dL5KdIQ z)xnt56`|b6AwIM9hvGpG7NKqxg_9j3flA&JO+5^QxN*)_l1-a!&&`e3ua#^+9GA!W47@5h1D7SxBBgz zj~g6#A;E5ZB*#GS*kPxQ7CM}hlTU9{TEnyYnAC=_HO#v^zb2pGSk5BU=z4(DjHTR3 zAyLlJBf=-b?}o<;1sN3+!CRc>oXjnOht!oOukhZYwoCBm5s4%t!}bUd9p!66{lfYJ z)g8`9%vFRsosHCYPGIufxFqC~&?UD^c|poAbym(c@sLuUeDYRG_9k}ADLM+;Ct9D( zeRBAT@e}DMil3sn<|a85F8LAsNF(S}mY3vHfu|-<*-~d1IfgEt6?AvS!Vp2z{wSxl z3%3QW4{To{Y*ISQ@@%u1r}xsei>6+d$`H@^p1J--$u7aWQ|0`oUkJZ+`lb7e=`WbS zl=JYmi8deaNY{{@Ex|64E?F+IeumF7$;;i6$3qOv4PVcAK9hfDerSZ&Hmw@biKn=x zdQDZEDy^j-vU1h3RrgkLhkgz{9n!xtF1U7`Tc~mHe>d_u9`{m@QFl-8KgT-vdGO8PyUV$j zi}~7GeO;%wDtoE)!rd*_s-Zv4yNvdDMa_T;rjSw=362#t!}?6!99TGea2*X~{}xO}JIWWUw(uB9K| z@NwJ8Z7a7+Zg_fw?e@BDx!Y zH{DKew)C;PC-y$^D*0L%`>^WL*SWJd&pz~a#qIU)%-&_bYrix9jLCe>d6CA&7Y?6r zeh~QdqCNNgsRr5i*VOqt`!}Xte?weiF{Ks~WZOrc( z-HVJrV}4-!iS4KT59iN|Z@uqaulzsdf7Sns49go-8Pl0N8yy>?nSTGMs!`nEXK!^o z_1mVKZykFU>~s`$>~C7z^z`6b#f%egCahKb{(Q!{iRUu;S_A4{T)V)Y_$;yea2=0& zYkO;Wn|)tFM}*Wx!|A5w7w~uqa%k{cmrdoEZ^s;PjneQ^$Wq(XB zE%MCOh`AA-Io>jsdRptu@^bdw|MT{9`=hy!xF1)G&(J>+TTr?1+@UY6%f(je3F%l( zbMWc#vzYS1Q9x^jSdZ!_;d0eJuF6BQhtiewFPdHKxZxb5CFl3aL(8{n(wm8UOe9?^Mp05k-A*loR+-^iPE^P#l9&$H9T$q+0Ijz(^k*ZKKu5}=2N#P zn9q4X|NmQ|LqZQHZCl#r9j+@I@oev!EXz$cg_XCt9*eSSw}r8-vDwkJO)K}R|LV8l z)7RDQRjcyNZj3v4s_EcX#_ZSJpLO=5U*2G_JbEWM>JG<72+!miM zT7N_1PQ_E{_WV=(XWp5zW##H4J5$c5T}pfT?4IttXufHyrazm`m&bea))wCTVO!pQ z{pNaGfBIRC+q;{$IqzJ&-)6_2)a577owc?PSrYOkt)mB z$?2cY_spF-x3*62=bePxi?&riPd;})i$DMUE`gGVmt6mMKeygveJhIR>@!u2w#o?{vP3ODqSM8(!-MR7pPJPya z@&%tS-f{oV*4ljj-^8!|{LZZXGy6}s@wUC5XJKb)`N+QL@5?{SozJJ7SDPpPZ_&Tg z>%_(M>wa8)ef`|U85b8$|1^E_{@j|Se_tJVExG>ou|vn?_MNOSHjMic_WSL@@;l|S z?-$?a_kU8OP_OYng-@;G0lqa=(r;GbffR= z6hj8^q!?^Wl-`(q<`SNg_^yat&Us^X?&Dvd^e*S;` zzv6dicJ^(ZW}Vq&ox_!IcK!RgFaMYx2tH7hAUA>KlY>+SBOjBmG3S*p#}{U^@m8P1eyj1B5cSya#U53mSJu|Hg`PJ5Q1&?f z+uXM~f2B73U1l0^?BAceSEFCezw;qjfG@l#v#prfMeRsL?^dy7UNPghQ5H?rtDY5y zKi!*C`$}BK>f5(%lmGiw9?(zsO8EbH>+IL_Yrd#IIb?WAIrfKD{FGZ#K0GOFH77OB zm~t>kO=TH1{ z{*ro5C~LmQ@0Mj2S+W9|^H2Pk{9Q@-cjSuJ7TyEy%^1FO?xVL$2a?@HE%u`+kpdzwuSGO(2bGUAcK-<*#o)?`8eTkcp31y|A;w zll#eHovByl4xGAlyr6M$*QPcr1rxht8C!TQZ=0O3uD+F2Y5(x$UH%OjS7T!`&V6|2 z{d{@j|7UyMm%ab)H)ri*g%rjFM@`0jjqmZF&T&jM^vM-eJD~pN?(}n?|Jw3TWBvbj zVW`K$DfT5Y0Zsxp3zyk-3404<1Rt2nq{(t@Qfo{b{cYtkRjnk~1|TKam7`Ti?YMRo)|(sX^$&LHm1 zIY&zR>&GQWrZTuVcRd_u;PDr}SEzC-JQlnY^+6(qo3+r7V2kzwst3dtWN`+_6z8mFb=D zTK0tetiQSkmv$VC^}JNNapKbL`4SHbG#PW(-o0I9C{nSAyt=1&-(I`!@8;B zej7j7D0*s5ZwacoS;U$BHqK$Xbx&Mgw!G4O?v-C+Ce7ZI9HYB9&kJ^S}kX7enc(`HNM|G)gS`PuVn6DQXEowjS~nYDhCN^F^A1*Lk; z*pj>7?i3cDut;OCx@ZuGo7?9-^-Fr^%{$Zh?0Vb$*uSf*^Hyt%on6oS{^Wm;-!B(F zd&{po`~0ua@4-{o%$O6f>OjlWPm1!UFqy7EUqwe!DGDO`wwP&S} z-{q-R^{jG2iT1Q5Ato3m8q4rxUpGj`=2kh&8LO0ZgShQUZZHe6Op)kO($dOzyVkXC&z|p}KK*`tC;#o{@B5y=+50|9oMF;-`RUWyycs8) z68_MA~3v6|4`oZ7f%q z%lFUTbMvL2Oo6i;1fw@u&eRbU@3K&ho}jnwe8&u_={ji=s%iUPG$>6IFFvFsUHdIE zCEajyaAl5Q_xU;J{8GDjdLN12+#(~WT&JXW>QT`q*BbXDQJRNB)^*(PN)EO>F0etL@s=d_*GJDZOwLf z_T;&T1UY(`XRb+6o9|oh?d_ZXsfaOR&*$`$9ogREqBoD-Id|;XjYCKNtakc){p^0t z!hG?Up%C22GWp;g>-~L0M{ohy1o+itWGIs6iT^z!`lGCRvWSRG$6)m`2QKI+W-t?n<+h5P_ zVqKx7?}Qf|o58TsPRpV0Eu#jPpu$D=BeDM%=hyu(5YVVN#Ft zSocECgv-})zw`S<7PW&R5vJw$~1KTIt3FbwLuqU7treyR74o7;rUC$e14ix#}l zNS?xcN?NolX`xZT>ww#Vp(`G*2n!K^WtJsk+Wf0UZSmBLZ5h>D_^p`cHk@-4UwHbW z`$gRs+AkcxSp4E;YhIXGVz^Br-$zA_b2Hm!+0F8odvO-wlQK2(=~zD9lQsO zHZtW1ZWF3zs%ErqkUhTds9<511@|0vJ+9-2MIW3h&I?CX8w>< zqqk2azWx1y?+=fE{LGTsq<3(lfRl$(j!KBYERJvve$MqBE{oVM_DQ%r5i%Fh?ojW% z-m&;dxr*c@#*@7wzCx4ACiG3zoAhpC+{BY9vYU)8yL1%ZD_ozfKUseAeXjr&AC(%F z8$z#DSE)v+vZ=&*JW}1N8mkiS{mgTk_q=IuCeN96$or+oPEW(7*E~bL=T4NGHf!oK zkJ;Yr9?QM*r{_)Br)R6#JoVzlj}sD4i=2{~xKqd2%~4~Ph&fmHrp_SuolcU=lUD2s zkvSt+ZRKoLX|>tP(Yn=sWi4$NN;2%Wh(u|=70K=R7U$=B&b3c@yYD*B zJcm5LIL~{L^OWOt?(5iV{Eur~_h8|KH5Q611>NWMW*5y?uq3KsuuG+489=iOBMwa*0 z9a+^^^j6p|K7A?nh2|HoD)Cyk*vPxlu`B8#_J-HSIfpBUEOuBN`FNt5vy_u z4lZ`{_B|c19ltv8^**z>*$d~c*z3PLy4ue@Ts(85)xA>j2?WTE? z_IvBhd8!uFGyCYwzbUlz42aM^C3-M;oSBWEw2adoDx!F0p;w77(OiT`%DHnN^x zbn8){>di}+i}t4Oe(J65tlf8&arN$NBH8t?b+666CZC&=`@_uFEO=YgwvF3Nw;kru zQl7AJZsA$J+~--_@4Z`d*ScD|y78~(@B1=6vXf+`WYng=(0iz}GTP|biiamkY~EOG znzGNN?B&9x%6_l>e3nNoS3fuFT+Z{w=Y;is>3Qm@$HeWJwewSKbjs?JqJRqYOkZ(r{{wSBYr(cw=kzt4A zTv=3k=ft;xH&$#|u{dzEwr$wESWCHF*;o$8Kpn2d6e}iEh2AC?V9H~>EGnOi5#C8d~9Y%d5?bge4hfJ8*>e&zL*j* z&*E(95i>o%eVdEc;<0!;_}kO(!2MK_N}#FRk7vCi{r_Al9i7)_m^M2eeJrr zhWQk;L%oNNtrWhfo>%+)Uu^Z{!pjfSqKZ#?76es%y7_(Meg3(hY_C?%D%|yw{jGdf zl!ornwE@|AuYIDEZhKUxRR5ounr>w+V-;0r^7G@3$gi8PK2JG6b)NbheVhFSiti7- zIr^ph@^m4+-qK?wb~|JWfBJ<7gufTPuYG-8Rovb?Z!gt8ob`C?=Id|Q&)<1)$HlVC z&(gB{OZ`v#U$fu(<<^&N-)?@}dVkaIs~7h#`TwWk@`0td-)&!i@8izm;)0(E{{voJ zxcl(M!->1!?4JBR=-u8u^Goah9c*iV(SEdliQH8Cu)3JvoBy3Zt8AwH-0iM=^}IC| zpAvdZzx|nhCf=?`+)mYAcYeU^74x^uu(5t>S^E0ssr1L^FV8>kFaB5SZ}wU4z2f$N zp8bhF?{nnL5AT1cTkmh*A5>BLX6x10=WZUjc|1Sp{+aWl`$H?L|HwX${r>yIcj3=B zo71dC?tCX`7$t6sWLD$G&3;#{LjG9 z@REU{)PRBERRRNp)eHs(@%%~gN8K107(4=eLR=Xb7<~Bz+9hPx$*Tx6v&@!JSSPR2 zE+ON~Ct$_JGh0T%ii<~&onxM;gcLK29y`Zn83ie37Ar2Eb@D1w%q;8VRo=?0{Qv(y z!+So+EYXr6zhDN_`FFV&|Ckr3eO6VbOF;UpkosS~?ahYU7#J8Blf2zs#JR7{=x1PH z;4JWnEM{Qf76xHPhFNnY7#J98JzX3_Brd1+dkP&^;9+4mn7zSZ#)AL$raG2Oxb7|s zZIVmqxwNc&vFNv$Yd-~&Kb(ux7GAp7(`9nRu9?OYX3B4Rd{0$ObE}wO-Q1>c5)P+M z9Qq)orf}XM(aqq6;EL`kTt~{99zUu0@}sQ7_RM_MGXIVCU&~j^KP~%vt?ujoUsjw+ zOg_iHF)%QwmbgZgq$HN4S|t~yCYGc!7#SEE=^7a78XAWf8Ce;bTN#*W8yH#{7(`w9 j*@vPbH$NpatrCj{Q;3G%e_m}23=9mOu6{1-oD!M(QWTKjuBBy{?hvLGUqJakmU0qqEPb}zQ?(1vl5e?Mg>EgP<{zP$6U-ROQ zu2a7sRxi%Jw&(k^v;VeUw|lQz`~GlIyvbvLx3(rf_nY4%M1;3f6T0AWQd-kD9X5C zB7=f(YKIF$hBd>Q^I=`C3=swlB5B@7EE#U-G8lMT#JFBV&*2Bohp{DjnN#sh3w(sdU6`)JjJy| z%o3RrXF2E{s+=il)M�PV*|l=S%D z)AOqf92giLmegJRr=!1Fs==DAA@|=R)pslzaoh_^zjvo?;&<4}u;Asb@XrOHuyI{D zVdu@8PoF%w;dR6*I`LF^?Z5Ox@dv{H=KQR)`SbnjgME)w8jWHb-C2G-ICSQV&r+H5 zlQ>(F?mX*v|FfO@{~y`QN=H1Cc1CE4JPs5p>~opb^Vw*rQm4}-&7-Q9{-->;uk)MP z?!GC*hAcbA>WR85Vw0J_d8-RVlpJ{Uoq^%g?)-ySG&qj}HTE=evN)dL5KdIQ z)xnt56`|b6AwIM9hvGpG7NKqxg_9j3flA&JO+5^QxN*)_l1-a!&&`e3ua#^+9GA!W47@5h1D7SxBBgz zj~g6#A;E5ZB*#GS*kPxQ7CM}hlTU9{TEnyYnAC=_HO#v^zb2pGSk5BU=z4(DjHTR3 zAyLlJBf=-b?}o<;1sN3+!CRc>oXjnOht!oOukhZYwoCBm5s4%t!}bUd9p!66{lfYJ z)g8`9%vFRsosHCYPGIufxFqC~&?UD^c|poAbym(c@sLuUeDYRG_9k}ADLM+;Ct9D( zeRBAT@e}DMil3sn<|a85F8LAsNF(S}mY3vHfu|-<*-~d1IfgEt6?AvS!Vp2z{wSxl z3%3QW4{To{Y*ISQ@@%u1r}xsei>6+d$`H@^p1J--$u7aWQ|0`oUkJZ+`lb7e=`WbS zl=JYmi8deaNY{{@Ex|64E?F+IeumF7$;;i6$3qOv4PVcAK9hfDerSZ&Hmw@biKn=x zdQDZEDy^j-vU1h3RrgkLhkgz{9n!xtF1U7`Tc~mHe>d_u9`{m@QFl-8KgT-vdGO8PyUV$j zi}~7GeO;%wDtoE)!rd*_s-Zv4yNvdDMa_T;rjSw=362#t!}?6!99TGea2*X~{}xO}JIWWUw(uB9K| z@NwJ8Z7a7+Zg_fw?e@BDx!Y zH{DKew)C;PC-y$^D*0L%`>^WL*SWJd&pz~a#qIU)%-&_bYrix9jLCe>d6CA&7Y?6r zeh~QdqCNNgsRr5i*VOqt`!}Xte?weiF{Ks~WZOrc( z-HVJrV}4-!iS4KT59iN|Z@uqaulzsdf7Sns49go-8Pl0N8yy>?nSTGMs!`nEXK!^o z_1mVKZykFU>~s`$>~C7z^z`6b#f%egCahKb{(Q!{iRUu;S_A4{T)V)Y_$;yea2=0& zYkO;Wn|)tFM}*Wx!|A5w7w~uqa%k{cmrdoEZ^s;PjneQ^$Wq(XB zE%MCOh`AA-Io>jsdRptu@^bdw|MT{9`=hy!xF1)G&(J>+TTr?1+@UY6%f(je3F%l( zbMWc#vzYS1Q9x^jSdZ!_;d0eJuF6BQhtiewFPdHKxZxb5CFl3aL(8{n(wm8UOe9?^Mp05k-A*loR+-^iPE^P#l9&$H9T$q+0Ijz(^k*ZKKu5}=2N#P zn9q4X|NmQ|LqZQHZCl#r9j+@I@oev!EXz$cg_XCt9*eSSw}r8-vDwkJO)K}R|LV8l z)7RDQRjcyNZj3v4s_EcX#_ZSJpLO=5U*2G_JbEWM>JG<72+!miM zT7N_1PQ_E{_WV=(XWp5zW##H4J5$c5T}pfT?4IttXufHyrazm`m&bea))wCTVO!pQ z{pNaGfBIRC+q;{$IqzJ&-)6_2)a577owc?PSrYOkt)mB z$?2cY_spF-x3*62=bePxi?&riPd;})i$DMUE`gGVmt6mMKeygveJhIR>@!u2w#o?{vP3ODqSM8(!-MR7pPJPya z@&%tS-f{oV*4ljj-^8!|{LZZXGy6}s@wUC5XJKb)`N+QL@5?{SozJJ7SDPpPZ_&Tg z>%_(M>wa8)ef`|U85b8$|1^E_{@j|Se_tJVExG>ou|vn?_MNOSHjMic_WSL@@;l|S z?-$?a_kU8OP_OYnRkbvN z2JTDdvj)Q?Tpy!y}QkxX}Vk8vQ95M_j7*n zdCTuH3%5S2oAktPGUtah@6%EL_p@6tyy^`~=$;a5~dvZS?K62#YlLH5=|Ns4VTk?U)2k`@cHJ>_V z?~P))$JgqlUibYF_Z!=i4i6o+r*kgu($jxZ^k?_(+4@ygA8g)Re0Z287{Pnsq`Q3e zn~X)PyKn3&U0_;OCpY)(B5|<`>$vBI-|`R^>{OUFTW}g{RPz1Z$8YxAKRfo|f#!o7 z=UG2||J%-A{`cS1@Cgm4QUWetez0qSNYu&$$xEm0_>&`X;#ydWH@BDOztVRyd-v{L z-e|whl-=*&w+9E;HFvUDO+PFx>8P)xV;=Nqm&24p`zwX*=N`MWt@nAKjALb7`2DHc zd3!xVm@9sV_uCYGIpD3oFfHwayW4^9;=QlCz8amct+$l8-(EBC(e?Obn=bKF2@aYN=Tb*uP#*}VJ5aaKadd0HE%$i548v=PB95^@Enls&KMNZkt&6CV6`0lcB!G@TBXU#h{JxnruaA~uY%#z&KVGZf48ErmxA9E|Qi|@Wu zcwt@l-#?#Czs}lGw$^Rh-L2eHPqX@F*V$c&>{>Q!_F_{h)`~^Dc+AbK(8d^aE%Z|IZIz&vyBLI4 zir8-3yQ%Q7LyMEf^`)Pt=rGUNQY$aXrRXcHsOr>_x@XqMEP<={xilv(>*Q(OP%EFX zNy?$fy7GRZDdynazo;^m8o z$$REhC`q|SiB)P!&YH#aVkO%H?X~y!TbQ+N(dmiMkucy{thTVo-!9Vp;;WObi3&=B zJrO*aku`>PeV2aK9#~&4QG3DO?wsw?2_G%of-iR|Tszc#Z1$`PA1%I5d35TA!~u5g zxozv#U-@W!QYAv{*{`oN&YnHd)+VaGSa16GJqsB(@02Y5TeU&woXX@MUb5@uLRPT4BzxtBz@1pZ_ZXKD$$na_L?(+X%Hodn_etu5&_U{jeWorHu zN}1dJ-o2P35$)s@A>_o><+}Fs=6&{taXNAT u&mTHenjp4=ImNr`%Bt|qzdirUGc=^M&R_i6;(@ literal 0 HcmV?d00001 diff --git a/examples/export/styles/darksun/js/darksun.js b/examples/export/styles/darksun/js/darksun.js new file mode 100644 index 0000000..99ce23f --- /dev/null +++ b/examples/export/styles/darksun/js/darksun.js @@ -0,0 +1,524 @@ +// darksun.js --- darksun JS file +// +// Copyright (C) 2011-2016 All Right Reserved, Fabrice Niessen +// +// This file is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// Author: Fabrice Niessen <(concat "fniessen" at-sign "pirilampo.org")> +// URL: https://github.com/fniessen/org-html-themes/ +// Version: 20140515.1841 + +$(function() { + $('p'). + html(function(index, old) { + return old.replace('FIXME', + 'FIXME'); + }); + $('p'). + html(function(index, old) { + return old.replace('XXX', + 'XXX'); + }); +}); + +// Remove leading section number +$(function() { + $('.section-number-2').text(""); + for (var i = 3; i <= 5; i++) { + $('.section-number-' + i).each(function() { + $(this).text($(this).text().replace(/^[0-9]+\./g, "")); + }); + } +}); + +$(function() { + $('
').prependTo('body'); +}); + +// generate contents of minitoc +function generateMiniToc(divId) { + $('#minitoc-content').empty().append('

In this section

    '); + $('#' + divId).find('h3').each(function(i) { + // [2018-09-03 Mon 03:48] Get text up to non-breaking space, + // to omit the tags. If there are no tags, there will be no + //  , so get all the text. (This fixes a bug in the + // original code.) + let text = null; + let pos = $(this).text().search(/ | ►/); + if (pos > 0) { + text = $(this).text().substring(0, pos); + } + else { + text = $(this).text(); + } + $("#minitoc-content>ul").append("
  • " + + text + "
  • "); + }); + // Ensure that the target is expanded (hideShow) + $('#minitoc-content a[href^="#"]').click(function() { + var href = $(this).attr('href'); + hsExpandAnchor(href); + }); +} + +// display tabs +function tabifySections() { + + // hide TOC (if present) + $('#table-of-contents').hide(); + + // grab the list of `h2' from the page + var allSections = []; + $('h2') + .each(function() { + // Remove TODO keywords and tags (contained in spans) + var tabText = $(this).clone().find('span').remove().end() + .text().trim(); + var tabId = $(this).parent().attr('id'); + if (tabText) { + // - remove heading number (all leading digits) + // - remove progress logging (between square brackets) + // - remove leading and trailing spaces + tabText = tabText.replace(/^\d+\s+/, '').replace(/\[[\d/%]+\]/, '').trim(); + + allSections.push({ + text: tabText, + id: tabId + }); + } + }); + + // create the tab links + var tabs = $('
      '); + for (i = 0; i < allSections.length; i++) { + var item = allSections[i]; + html = $('
    • ' + item.text + '
    • '); + tabs.append(html); + } + + // insert tabs menu after title (`h1'), or at the beginning of the content + if($('.title').length !== 0) { + $('.title').after(tabs); + } + else { + $('#content').prepend(tabs); + } +} + +function selectTabAndScroll(href) { + // At this point we assume that href is local (starts with #) + // alert(href); + + // Find the tab to activate + var targetTab = $(href).closest('.ui-tabs-panel'); + var targetTabId = targetTab.attr('id'); + var targetTabAriaLabel = targetTab.attr('aria-labelledby'); + + var targetTabIndex = $("#content ul li") + .index($('[aria-labelledby="' + targetTabAriaLabel + '"]')); + + // Activate target tab + $('#content').tabs('option', 'active', targetTabIndex); + + // Rebuild minitoc + generateMiniToc(targetTabId); + + // Set the location hash + // document.location.hash = href; + + // Scroll to top if href was a tab + if (href == '#' + targetTabId) { + // alert(targetTabId); + $.scrollTo(0); + } + // Scroll to href if href was not a tab + else { + $.scrollTo(href); + } +} + +$(document).ready(function() { + $('#preamble').remove(); + $('#table-of-contents').remove(); + + // Initialize hideShow + hsInit(); + + // Prepare for tabs + tabifySections(); + + // Build the tabs from the #content div + $('#content').tabs(); + + // Set default animation + $('#content').tabs('option', 'show', true); + + // Rebuild minitoc when a tab is activated + $('#content').tabs({ + activate: function(event, ui) { + var divId = ui.newTab.attr('aria-controls'); + generateMiniToc(divId); + } + }); + + // Required to get the link of the tab in URL + $('#content ul').localScroll({ + target: '#content', + duration: 0, + hash: true + }); + + // Handle hash in URL + if ($('#content') && document.location.hash) { + hsExpandAnchor(document.location.hash); + selectTabAndScroll(document.location.hash); + } + // If no hash, build the minitoc anyway for selected tab + else { + var divId = $('#content div[aria-expanded=true]').attr('id'); + generateMiniToc(divId); + } + + // Handle click on internal links + $('.ui-tabs-panel a[href^="#"]').click(function(e) { + var href = $(this).attr('href'); + hsExpandAnchor(href); + selectTabAndScroll(href); + e.preventDefault(); + }); + + // add sticky headers to tables + $('table').stickyTableHeaders(); +}); + +function copyToClipboard(text) +{ + if (window.clipboardData && window.clipboardData.setData) { // Internet Explorer + window.clipboardData.setData("Text", text); + } + else { // Fallback solution + window.prompt("Copy to clipboard: Ctrl+C, Enter", text); + } +} + +$(document).ready(function() { + // Assuming that the ZeroClipboard swf file is in the same folder than darksun, + // get the path to it (it will be relative to the current page location). + var bbScriptPath = $('script[src$="darksun.js"]').attr('src'); // the js file path + var bbPathToZeroClipboardSwf = bbScriptPath.replace('darksun.js', 'ZeroClipboard.swf'); + + // Add copy to clipboard snippets + $('.org-src-container').prepend('
      [copy]
      '); + + // Display/hide snippets on source block mouseenter/mouseleave + $(document).on('mouseenter', '.org-src-container', function () { + $(this).find('.snippet-copy-to-clipboard').show(); + + // Need to call zclip here, once the button is visible. + // Beacause when the button is not visible, zclip does nothing. + if ((window.location.protocol != 'file:') && ($(this).find('.zclip').length == 0)) { + $(this).find('.copy-to-clipboard-button').zclip({ + //path: 'http://www.steamdev.com/zclip/js/ZeroClipboard.swf', + //path: 'styles/darksun/js/ZeroClipboard.swf', + path: bbPathToZeroClipboardSwf, + copy: function() { + return $(this).parent().parent().find('.src').text(); + } + }); + } + }).on('mouseleave', '.org-src-container', function () { + $(this).find('.snippet-copy-to-clipboard').hide(); + }); + + // Handle copy to clipboard (here, for a local file only 'file://...' + if (window.location.protocol == 'file:') { // if local file use browser-specific code + $('.copy-to-clipboard-button').click(function() { + // Get the text to be copied + var text = $(this).parent().parent().find('.src').text(); + text = text.replace(/\n/g, "\r\n"); + // alert(text); + copyToClipboard(text); + }); + } +}); + +$(function() { + $('li > code :contains("[X]")') + .parent() + .addClass('checked') + .end() + .remove(); + $('li > code :contains("[-]")') + .parent() + .addClass('halfchecked') + .end() + .remove(); + $('li > code :contains("[ ]")') + .parent() + .addClass('unchecked') + .end() + .remove(); +}); + +$(function() { + $('i :contains("[#A]")') + .replaceWith('[#A]'); + $('i :contains("[#B]")') + .replaceWith('[#B]'); + $('i :contains("[#C]")') + .replaceWith('[#C]'); +}); + +function togglePanel(e) { + e.preventDefault(); + + $("#left-panel-contents").toggleClass('active').toggle(200); + $("#right-panel-contents").toggleClass('active').toggle(200); + + var slidePos = + $("#left-panel-button").css("left") == "-23px"? '182px': '-23px'; + + $("#left-panel-button"). + animate({"left": slidePos, "opacity": 0.9}, {duration: "200" }); + + // if ($("#left-panel-contents").hasClass('active')) { + // hsHideTodoKeyword('done'); + // } else { + // hsShowTodoKeyword('done'); + // } + + // return false; +} + +$(function() { + $('') + .appendTo('body'); + + $('
      ') + .appendTo('body'); + + $('#left-panel-button').click(togglePanel); +}); + +$(function() { + var ul = $('
        ').appendTo('#left-panel-contents'); + var countOfTodo = {}, listOfTodo = [], totalOfTodo = 0; + + // assign the counts (avoid double-counting elements from the ToC) + $('span.todo').not($('#table-of-contents span.todo')).each(function() { + var $thisTodo = $(this).text().trim(); + + if ($.inArray($thisTodo, listOfTodo) == -1) { + countOfTodo[$thisTodo] = 1; + listOfTodo.push($thisTodo); + } + else + countOfTodo[$thisTodo] += 1; + totalOfTodo += 1; + }); + + function scoreTodo(t) { + switch (t) { + case 'NEW': return 1; + case 'TODO': return 2; + case 'STRT': return 3; + case 'WAIT': return 4; + case 'DLGT': return 5; + case 'SDAY': return 6; + case 'DFRD': return 7; + case 'DONE': return 8; + case 'CANX': return 9; + default: return 0; + } + } + + function compareTodo(a, b) { + if (scoreTodo(a) < scoreTodo(b)) return -1; + if (scoreTodo(a) > scoreTodo(b)) return 1; + return 0; + } + + listOfTodo.sort(compareTodo); + + // display + for (i = 0; i < listOfTodo.length; i++) { + var $thisTodo = listOfTodo[i]; + $(ul).append('
      • '+ $thisTodo + '' + + ' (' + countOfTodo[$thisTodo] + ')
      • '); + } + + $('#listOfTodo') + .before('Next Actions (' + totalOfTodo + '):
        '); +}); + +$(function() { + var ul = $('
          ').appendTo('#left-panel-contents'); + var countOfDone = {}, listOfDone = [], totalOfDone = 0; + + // assign the counts (avoid double-counting elements from the ToC) + $('span.done').not($('#table-of-contents span.done')).each(function() { + var $thisDone = $(this).text().trim(); + + if ($.inArray($thisDone, listOfDone) == -1) { + countOfDone[$thisDone] = 1; + listOfDone.push($thisDone); + } + else + countOfDone[$thisDone] += 1; + totalOfDone += 1; + }); + + // display + for (i = 0; i < listOfDone.length; i++) { + var $thisDone = listOfDone[i]; + $(ul).append('
        • '+ $thisDone + '' + + ' (' + countOfDone[$thisDone] + ')
        • '); + } + + $('#listOfDone') + .before('Done Actions (' + totalOfDone + '):
          '); +}); + +$(function() { + var ul = $('
            ').appendTo('#right-panel-contents'); + var countOfTags = {}, listOfTags = [], totalOfTags = 0; + + // assign the counts (avoid double-counting elements from the ToC) + $('span.tag').not($('#table-of-contents span.tag')).each(function() { + var $thisTagGroup = $(this).text().trim().split(/\s/); + // \s matches spaces, tabs, new lines, etc. + + for (tag in $thisTagGroup) { + if ($.inArray($thisTagGroup[tag], listOfTags) == -1) { + countOfTags[$thisTagGroup[tag]] = 1; + listOfTags.push($thisTagGroup[tag]); + } + else + countOfTags[$thisTagGroup[tag]] += 1; + totalOfTags += 1; + } + }); + + listOfTags.sort(); + + // display + for (i = 0; i < listOfTags.length; i++) { + var $thisTag = listOfTags[i]; + // $(ul).append('
          • '+ + // $thisTag + ' (' + countOfTags[$thisTag] + ')
          • '); + $(ul).append('
          • ' + $thisTag + + '' + + ' (' + countOfTags[$thisTag] + ')
          • '); + } + + $('#listOfTags') + .before('Contexts (' + totalOfTags + '):
            '); +}); + +$(function() { + $('.done').parent(':header').parent().find(':header').addClass('DONEheader'); + $('.done').parent(':header').parent().css({color: '#999999'}); +}); + +$(function() { + $('span.todo').click(function(e) { + var orgKeyword = $(this).text().trim(); + $('.' + orgKeyword).toggleClass('selected'); + $('#content .' + orgKeyword).parent().parent() + .toggleClass('match' + orgKeyword); + $('#left-panel-wrapper .' + orgKeyword).parent() + .toggleClass('match' + orgKeyword); + e.preventDefault(); + }) +}); + +$(function() { + $('.tag span').click(function(e) { + var orgTag = $(this).text().trim(); + $('.' + orgTag).toggleClass('selected'); + $('#content .' + orgTag).parent().parent().parent() + .toggleClass('matchtag'); + $('#right-panel-wrapper .' + orgTag).parent().parent() + .toggleClass('matchtag'); + e.preventDefault(); + }) +}); + +function clickPreviousTab() { + var active = $('#content').tabs('option', 'active'); + // Avoid going back to last tab + if (active == 0) return; + + $('#content').tabs('option', 'active', active - 1); + + // Set the location href + var href = $('#content div[aria-expanded=true]').attr('id'); + document.location.hash = href; + $.scrollTo(0); +} + +function clickNextTab() { + var active = $('#content').tabs('option', 'active'); + $('#content').tabs('option', 'active', active + 1); + + // Set the location href + var href = $('#content div[aria-expanded=true]').attr('id'); + document.location.hash = href; + $.scrollTo(0); +} + +function orgDefkey(e) { + if (!e) + var e = window.event; + var keycode = (e.keyCode) ? e.keyCode : e.which; + var actualkey = String.fromCharCode(keycode); + switch (actualkey) { + case "?": // help (dashboard) + case "h": + togglePanel(e); + break; + case "n": // next + clickNextTab(); + break; + case "p": // previous + clickPreviousTab(); + break; + // case "b": // scroll down - should be mapped to Shift-SPC + // $(window).scrollTop($(window).scrollTop()-$(window).height()); + // break; + case "<": // scroll to top + $(window).scrollTop(0); + break; + case ">": // scroll to bottom + $(window).scrollTop($(document).height()); + break; + case "-": // collapse all + hsCollapseAll(); + break; + case "+": // expand all + hsExpandAll(); + break; + case "r": // go to next task + hsReviewTaskNext(); + break; + case "R": // go to previous task + hsReviewTaskPrev(); + break; + case "q": // quit reviewing + hsReviewTaskQuit(); + break; + case "g": // refresh the page (from the server, rather than the cache) + location.reload(true); + break; + } +} + +document.onkeypress = orgDefkey; diff --git a/examples/export/styles/darksun/js/hideshow.js b/examples/export/styles/darksun/js/hideshow.js new file mode 100644 index 0000000..e528759 --- /dev/null +++ b/examples/export/styles/darksun/js/hideshow.js @@ -0,0 +1,335 @@ +// hideshow.js --- HideShow JS file +// +// Copyright (C) 2014 All Right Reserved, Fabrice Niessen +// +// This file is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// Author: Fabrice Niessen <(concat "fniessen" at-sign "pirilampo.org")> +// URL: https://github.com/fniessen/hide-show/ +// Version: 20140912.1722 + +// var HS_COLLAPSIBLE_HEADERS = $('h3, h4, h5'); // collapsible headers + +if (typeof HS_STARTUP_FOLDED === 'undefined') { + var HS_STARTUP_FOLDED = false; // Show just the overview, or show all. +} + +if (typeof HS_SHOW_ALL_TEXT === 'undefined') { + var HS_SHOW_ALL_TEXT = 'Expand'; +} +if (typeof HS_HIDE_ALL_TEXT === 'undefined') { + var HS_HIDE_ALL_TEXT = 'Collapse'; +} + +if (typeof HS_ALWAYS_DISPLAY_ICON === 'undefined') { + var HS_ALWAYS_DISPLAY_ICON = false; // Display an icon for all states, or + // just when closed. +} + +if (typeof HS_ICON_CLOSED === 'undefined') { + var HS_ICON_CLOSED = '►'; // black right-pointing pointer +} +if (typeof HS_ICON_OPEN === 'undefined') { + var HS_ICON_OPEN = '▼'; // black down-pointing triangle +} +if (typeof HS_ICON_EMPTY === 'undefined') { + var HS_ICON_EMPTY = '■'; // black square +} + +if (typeof HS_SHOW_ALL_OPEN_DONE_TREES === 'undefined') { + var HS_SHOW_ALL_OPEN_DONE_TREES = false; // Expand all will open DONE trees. +} + +if (typeof HS_CLASS === 'undefined') { + var HS_CLASS = 'hsCollapsible'; +} + +// Expand a header +function hsExpand(header) { + hsExpand2(header, true); +} + +// Expand a header +function hsExpand2(header, expandDoneHeader) { + // Ignore non collapsible entries + if (!header.parent().hasClass(HS_CLASS)) return; + + // Do not expand DONE node if not required + if (hsIsDoneHeader(header) && !expandDoneHeader) return; + + header.children('span[class="ellipsis"]').remove(); + if (HS_ALWAYS_DISPLAY_ICON == true) { + header.append(' ' + HS_ICON_OPEN + ''); + } + header.parent().removeClass('hsCollapsed').addClass('hsExpanded'); + header.nextAll().show(); +} + +// Expand a header and all its parents +function hsExpandParents(header) { + hsExpand(header); + header.parents('.hsCollapsed').each(function() { + hsExpand($(this).children(':header')); + }); +} + +// Collapse a header and all its parents +function hsCollapseParents(header) { + hsCollapse(header); + header.parents('.hsExpanded').each(function() { + hsCollapse($(this).children(':header')); + }); +} + +// Collapse a header +function hsCollapse(header) { + // Ignore non collapsible entries + if (!header.parent().hasClass(HS_CLASS)) return; + + header.children('span[class="ellipsis"]').remove(); + header.append(' ' + HS_ICON_CLOSED + ''); + header.parent().removeClass('hsExpanded').addClass('hsCollapsed'); + // header.nextAll().hide('fast'); + header.nextAll().hide(); +} + +// Toggle a header +function hsToggleCollapsing(header) { + if (header.parent().hasClass('hsCollapsed')) + hsExpand(header); + else if (header.parent().hasClass('hsExpanded')) + hsCollapse(header); +} + +// Expand all headers +function hsExpandAll() { + $('#content .hsCollapsed').each(function() { + hsExpand2($(this).children(':header'), HS_SHOW_ALL_OPEN_DONE_TREES); + }); +} + +// Collapse all headers +function hsCollapseAll() { + $('#content .hsExpanded').each(function() { + hsCollapse($(this).children(':header')); + }); +} + +// Collapse all visible headers +function hsCollapseAllVisible() { + $('#content .hsExpanded:visible').each(function() { + hsCollapse($(this).children(':header')); + }); +} + +// Add click events to H3/H4/H5 headers which have contents. +function hsInit() { + for (var i = 3; i <= 5; i++) { + $('#content .outline-' + i).each(function() { + var header = $(this).children(':header'); + if (header.siblings().length > 0) { + $(this).addClass(HS_CLASS); + header.css({cursor: 'pointer'}); + header.click(function() { + hsToggleCollapsing($(this)); }); + + // Allow to override global Collapse/Expand default on an entry + // basis (see property `:HTML_CONTAINER_CLASS:') + if (header.parent().hasClass('hsCollapsed')) { + hsCollapse(header); + } else if (header.parent().hasClass('hsExpanded')) { + hsExpand(header); + } else { + hsSetDefaultVisibility(header); + } + } + else { + if (HS_ALWAYS_DISPLAY_ICON == true) { + header.append(' ' + HS_ICON_EMPTY + + ''); + } + $(this).addClass('hsEmpty'); + } + }); + } + + // Add div to hold minitoc content (so it can be cleared without deleting the buttons) + $('#minitoc').append($('
            ')); + + // Add buttons + $('#minitoc').append($('
            ')); + $('.buttons').append($('' + HS_SHOW_ALL_TEXT + '') + .addClass('hsButton') + .click(hsExpandAll)); + $('.buttons').append($('' + HS_HIDE_ALL_TEXT + '') + .addClass('hsButton') + .click(hsCollapseAll)); +} + +// Returns true if a header is a DONE header +function hsIsDoneHeader(header) { + return $('span.done', header).length; +} + +// Sets the default visibility state to a header +function hsSetDefaultVisibility(header) { + if (HS_STARTUP_FOLDED) { + hsCollapse(header); + } + else { + if (!hsIsDoneHeader(header) | HS_SHOW_ALL_OPEN_DONE_TREES) { + hsExpand(header); + } + else { + hsCollapse(header); + } + } +} + +// Expands an anchor, i.e. expand all parent headers +function hsExpandAnchor(id) { + // alert(id); + if (id) { + // alert($(id + '.hsNode').length); + $(id).parents('.hsCollapsed').each(function() { + hsExpand2($(this).children(':header'), true); + }); + } +} + +// Search for next task to review, starting from beginning of current tab. +// If BACKWARD is true, search in the reverse direction. +function hsReviewTaskNext(backward) { + // If no tasks to review at all, do nothing. + if ($('.outline-2 span.todo').length == 0) return; + + if ($('.hsReviewing').length == 0) { // reviewing is starting + $('body').addClass('hsReviewing'); + $('.hsReviewPanel').addClass('hsReviewing'); + $('body').prepend('
            '); + hsAddReviewingPanels(); + + hsCollapseAll(); + // Get first task to review on each tab and expand it + $('.outline-2').each(function(){ + var firstTodo = $(this).find('span.todo:first').parent().parent(); + firstTodo.addClass('hsReview'); + hsExpandParents($(firstTodo).children(':header')); + }); + } + else { + // Get all todos + var todosElements = $('.outline-2:visible span.todo').parent().parent(); + // alert(todosElements.length); + var todos = jQuery.makeArray(todosElements); + if (backward) { + todos = todos.reverse(); + } + + // Find current review item and review the next one + var foundReview = false; + var index; + for (index = 0; index < todos.length; ++index) { + var todo = todos[index]; + if (foundReview) { + $(todo).addClass('hsReview'); + hsExpandParents($(todo).children(':header')); + break; + } + if ($(todo).hasClass('hsReview')) { + foundReview = true; + if (index < todos.length - 1) { + $(todo).removeClass('hsReview'); + hsCollapseParents($(todo).children(':header')); + } + } + } + + // Update reviewing panel + if (index < todos.length) { + var reviewItem = index + 1; + if (backward) { + reviewItem = todos.length - index; + } + + $('.outline-2:visible .hsReviewingPanel .hsReviewItem').text(reviewItem); + } + } + + // Scroll to the current review item + $('html, body').animate({ + scrollTop: $(".hsReview:visible").offset().top + }, 200); +} + +// go to previous task to review +function hsReviewTaskPrev() { + hsReviewTaskNext(true); +} + +// stop reviewing tasks +function hsReviewTaskQuit() { + $('body').removeClass('hsReviewing'); + $('.hsReviewPanel').removeClass('hsReviewing'); + $('.hsReview').removeClass('hsReview'); + $('#hsOverlay').remove(); + $('.hsReviewingPanel').remove(); +} + +function hsHideTodoKeyword(kw) { + $('span.' + kw).addClass('hsHidden').parent().parent().hide(); +} + +function hsShowTodoKeyword(kw) { + $('span.' + kw).removeClass('hsHidden').parent().parent().show(); + // XXX Show if parent is not collapsed! +} + +function hsAddReviewPanels() { + $('.outline-2').each(function(e) { + var nbItems = $(this).find('span.todo').length; + if (nbItems > 0) { + $(this).prepend('
            ' + + 'Press r or click here
            ' + + 'to review ' + nbItems + ' tasks
            ' + + '(out of ' + nbReviewTotalTasks + ' tasks)
            '); + } + }); +} + +function hsAddReviewingPanels() { + $('.outline-2').each(function(e) { + var nbItems = $(this).find('span.todo').length; + if (nbItems > 0) { + $(this).prepend('
            Reviewing task 1 / ' + nbItems + '
            ' + + '(out of ' + nbReviewTotalTasks + ' tasks)
            ' + + 'Shortcuts: ' + + 'r (next) - ' + + 'R (previous)' + + '
            '); + } + else { + $(this).prepend('
            No task to review
            ' + + '(out of ' + nbReviewTotalTasks + ' tasks)
            ' + + 'Shortcuts: ' + + 'r (next) - ' + + 'R (previous)' + + '
            '); + } + }); +} + +var nbReviewTotalTasks; + +$(document).ready(function() { + nbReviewTotalTasks = $('.outline-2 span.todo').length; + hsAddReviewPanels(); +}); diff --git a/examples/export/styles/darksun/js/jquery-1.11.0.min.js b/examples/export/styles/darksun/js/jquery-1.11.0.min.js new file mode 100644 index 0000000..73f33fb --- /dev/null +++ b/examples/export/styles/darksun/js/jquery-1.11.0.min.js @@ -0,0 +1,4 @@ +/*! jQuery v1.11.0 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k="".trim,l={},m="1.11.0",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(n.isPlainObject(c)||(b=n.isArray(c)))?(b?(b=!1,f=a&&n.isArray(a)?a:[]):f=a&&n.isPlainObject(a)?a:{},g[d]=n.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray||function(a){return"array"===n.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return a-parseFloat(a)>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==n.type(a)||a.nodeType||n.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(l.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&n.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:k&&!k.call("\ufeff\xa0")?function(a){return null==a?"":k.call(a)}:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),n.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||n.guid++,e):void 0},now:function(){return+new Date},support:l}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s="sizzle"+-new Date,t=a.document,u=0,v=0,w=eb(),x=eb(),y=eb(),z=function(a,b){return a===b&&(j=!0),0},A="undefined",B=1<<31,C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=D.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},J="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",K="[\\x20\\t\\r\\n\\f]",L="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",M=L.replace("w","w#"),N="\\["+K+"*("+L+")"+K+"*(?:([*^$|!~]?=)"+K+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+M+")|)|)"+K+"*\\]",O=":("+L+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+N.replace(3,8)+")*)|.*)\\)|)",P=new RegExp("^"+K+"+|((?:^|[^\\\\])(?:\\\\.)*)"+K+"+$","g"),Q=new RegExp("^"+K+"*,"+K+"*"),R=new RegExp("^"+K+"*([>+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(O),U=new RegExp("^"+M+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L.replace("w","w*")+")"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+O),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=/'|\\/g,ab=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),bb=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{G.apply(D=H.call(t.childNodes),t.childNodes),D[t.childNodes.length].nodeType}catch(cb){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function db(a,b,d,e){var f,g,h,i,j,m,p,q,u,v;if((b?b.ownerDocument||b:t)!==l&&k(b),b=b||l,d=d||[],!a||"string"!=typeof a)return d;if(1!==(i=b.nodeType)&&9!==i)return[];if(n&&!e){if(f=Z.exec(a))if(h=f[1]){if(9===i){if(g=b.getElementById(h),!g||!g.parentNode)return d;if(g.id===h)return d.push(g),d}else if(b.ownerDocument&&(g=b.ownerDocument.getElementById(h))&&r(b,g)&&g.id===h)return d.push(g),d}else{if(f[2])return G.apply(d,b.getElementsByTagName(a)),d;if((h=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(h)),d}if(c.qsa&&(!o||!o.test(a))){if(q=p=s,u=b,v=9===i&&a,1===i&&"object"!==b.nodeName.toLowerCase()){m=ob(a),(p=b.getAttribute("id"))?q=p.replace(_,"\\$&"):b.setAttribute("id",q),q="[id='"+q+"'] ",j=m.length;while(j--)m[j]=q+pb(m[j]);u=$.test(a)&&mb(b.parentNode)||b,v=m.join(",")}if(v)try{return G.apply(d,u.querySelectorAll(v)),d}catch(w){}finally{p||b.removeAttribute("id")}}}return xb(a.replace(P,"$1"),b,d,e)}function eb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function fb(a){return a[s]=!0,a}function gb(a){var b=l.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function hb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function ib(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||B)-(~a.sourceIndex||B);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function jb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function kb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function lb(a){return fb(function(b){return b=+b,fb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function mb(a){return a&&typeof a.getElementsByTagName!==A&&a}c=db.support={},f=db.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},k=db.setDocument=function(a){var b,e=a?a.ownerDocument||a:t,g=e.defaultView;return e!==l&&9===e.nodeType&&e.documentElement?(l=e,m=e.documentElement,n=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){k()},!1):g.attachEvent&&g.attachEvent("onunload",function(){k()})),c.attributes=gb(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=gb(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(e.getElementsByClassName)&&gb(function(a){return a.innerHTML="
            ",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=gb(function(a){return m.appendChild(a).id=s,!e.getElementsByName||!e.getElementsByName(s).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==A&&n){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ab,bb);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ab,bb);return function(a){var c=typeof a.getAttributeNode!==A&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==A?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==A&&n?b.getElementsByClassName(a):void 0},p=[],o=[],(c.qsa=Y.test(e.querySelectorAll))&&(gb(function(a){a.innerHTML="",a.querySelectorAll("[t^='']").length&&o.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||o.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll(":checked").length||o.push(":checked")}),gb(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&o.push("name"+K+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||o.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),o.push(",.*:")})),(c.matchesSelector=Y.test(q=m.webkitMatchesSelector||m.mozMatchesSelector||m.oMatchesSelector||m.msMatchesSelector))&&gb(function(a){c.disconnectedMatch=q.call(a,"div"),q.call(a,"[s!='']:x"),p.push("!=",O)}),o=o.length&&new RegExp(o.join("|")),p=p.length&&new RegExp(p.join("|")),b=Y.test(m.compareDocumentPosition),r=b||Y.test(m.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},z=b?function(a,b){if(a===b)return j=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===t&&r(t,a)?-1:b===e||b.ownerDocument===t&&r(t,b)?1:i?I.call(i,a)-I.call(i,b):0:4&d?-1:1)}:function(a,b){if(a===b)return j=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],k=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:i?I.call(i,a)-I.call(i,b):0;if(f===g)return ib(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)k.unshift(c);while(h[d]===k[d])d++;return d?ib(h[d],k[d]):h[d]===t?-1:k[d]===t?1:0},e):l},db.matches=function(a,b){return db(a,null,null,b)},db.matchesSelector=function(a,b){if((a.ownerDocument||a)!==l&&k(a),b=b.replace(S,"='$1']"),!(!c.matchesSelector||!n||p&&p.test(b)||o&&o.test(b)))try{var d=q.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return db(b,l,null,[a]).length>0},db.contains=function(a,b){return(a.ownerDocument||a)!==l&&k(a),r(a,b)},db.attr=function(a,b){(a.ownerDocument||a)!==l&&k(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!n):void 0;return void 0!==f?f:c.attributes||!n?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},db.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},db.uniqueSort=function(a){var b,d=[],e=0,f=0;if(j=!c.detectDuplicates,i=!c.sortStable&&a.slice(0),a.sort(z),j){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return i=null,a},e=db.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=db.selectors={cacheLength:50,createPseudo:fb,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ab,bb),a[3]=(a[4]||a[5]||"").replace(ab,bb),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||db.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&db.error(a[0]),a},PSEUDO:function(a){var b,c=!a[5]&&a[2];return V.CHILD.test(a[0])?null:(a[3]&&void 0!==a[4]?a[2]=a[4]:c&&T.test(c)&&(b=ob(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ab,bb).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=w[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&w(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==A&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=db.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),t=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&t){k=q[s]||(q[s]={}),j=k[a]||[],n=j[0]===u&&j[1],m=j[0]===u&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[u,n,m];break}}else if(t&&(j=(b[s]||(b[s]={}))[a])&&j[0]===u)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(t&&((l[s]||(l[s]={}))[a]=[u,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||db.error("unsupported pseudo: "+a);return e[s]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?fb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:fb(function(a){var b=[],c=[],d=g(a.replace(P,"$1"));return d[s]?fb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:fb(function(a){return function(b){return db(a,b).length>0}}),contains:fb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:fb(function(a){return U.test(a||"")||db.error("unsupported lang: "+a),a=a.replace(ab,bb).toLowerCase(),function(b){var c;do if(c=n?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===m},focus:function(a){return a===l.activeElement&&(!l.hasFocus||l.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:lb(function(){return[0]}),last:lb(function(a,b){return[b-1]}),eq:lb(function(a,b,c){return[0>c?c+b:c]}),even:lb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:lb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:lb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:lb(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function qb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=v++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[u,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[s]||(b[s]={}),(h=i[d])&&h[0]===u&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function rb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function sb(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function tb(a,b,c,d,e,f){return d&&!d[s]&&(d=tb(d)),e&&!e[s]&&(e=tb(e,f)),fb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||wb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:sb(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=sb(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?I.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=sb(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ub(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],i=g||d.relative[" "],j=g?1:0,k=qb(function(a){return a===b},i,!0),l=qb(function(a){return I.call(b,a)>-1},i,!0),m=[function(a,c,d){return!g&&(d||c!==h)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>j;j++)if(c=d.relative[a[j].type])m=[qb(rb(m),c)];else{if(c=d.filter[a[j].type].apply(null,a[j].matches),c[s]){for(e=++j;f>e;e++)if(d.relative[a[e].type])break;return tb(j>1&&rb(m),j>1&&pb(a.slice(0,j-1).concat({value:" "===a[j-2].type?"*":""})).replace(P,"$1"),c,e>j&&ub(a.slice(j,e)),f>e&&ub(a=a.slice(e)),f>e&&pb(a))}m.push(c)}return rb(m)}function vb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,i,j,k){var m,n,o,p=0,q="0",r=f&&[],s=[],t=h,v=f||e&&d.find.TAG("*",k),w=u+=null==t?1:Math.random()||.1,x=v.length;for(k&&(h=g!==l&&g);q!==x&&null!=(m=v[q]);q++){if(e&&m){n=0;while(o=a[n++])if(o(m,g,i)){j.push(m);break}k&&(u=w)}c&&((m=!o&&m)&&p--,f&&r.push(m))}if(p+=q,c&&q!==p){n=0;while(o=b[n++])o(r,s,g,i);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=E.call(j));s=sb(s)}G.apply(j,s),k&&!f&&s.length>0&&p+b.length>1&&db.uniqueSort(j)}return k&&(u=w,h=t),r};return c?fb(f):f}g=db.compile=function(a,b){var c,d=[],e=[],f=y[a+" "];if(!f){b||(b=ob(a)),c=b.length;while(c--)f=ub(b[c]),f[s]?d.push(f):e.push(f);f=y(a,vb(e,d))}return f};function wb(a,b,c){for(var d=0,e=b.length;e>d;d++)db(a,b[d],c);return c}function xb(a,b,e,f){var h,i,j,k,l,m=ob(a);if(!f&&1===m.length){if(i=m[0]=m[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&c.getById&&9===b.nodeType&&n&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(ab,bb),b)||[])[0],!b)return e;a=a.slice(i.shift().value.length)}h=V.needsContext.test(a)?0:i.length;while(h--){if(j=i[h],d.relative[k=j.type])break;if((l=d.find[k])&&(f=l(j.matches[0].replace(ab,bb),$.test(i[0].type)&&mb(b.parentNode)||b))){if(i.splice(h,1),a=f.length&&pb(i),!a)return G.apply(e,f),e;break}}}return g(a,m)(f,b,!n,e,$.test(a)&&mb(b.parentNode)||b),e}return c.sortStable=s.split("").sort(z).join("")===s,c.detectDuplicates=!!j,k(),c.sortDetached=gb(function(a){return 1&a.compareDocumentPosition(l.createElement("div"))}),gb(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||hb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&gb(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||hb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),gb(function(a){return null==a.getAttribute("disabled")})||hb(J,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),db}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return n.inArray(a,b)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;e>b;b++)if(n.contains(d[b],this))return!0}));for(b=0;e>b;b++)n.find(a,d[b],c);return c=this.pushStack(e>1?n.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=a.document,A=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,B=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:A.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:z,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=z.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return y.find(a);this.length=1,this[0]=d}return this.context=z,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};B.prototype=n.fn,y=n(z);var C=/^(?:parents|prev(?:Until|All))/,D={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!n(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b,c=n(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(n.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?n.inArray(this[0],n(a)):n.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function E(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return E(a,"nextSibling")},prev:function(a){return E(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return n.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(D[a]||(e=n.unique(e)),C.test(a)&&(e=e.reverse())),this.pushStack(e)}});var F=/\S+/g,G={};function H(a){var b=G[a]={};return n.each(a.match(F)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?G[a]||H(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&n.each(arguments,function(a,c){var d;while((d=n.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){if(a===!0?!--n.readyWait:!n.isReady){if(!z.body)return setTimeout(n.ready);n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(z,[n]),n.fn.trigger&&n(z).trigger("ready").off("ready"))}}});function J(){z.addEventListener?(z.removeEventListener("DOMContentLoaded",K,!1),a.removeEventListener("load",K,!1)):(z.detachEvent("onreadystatechange",K),a.detachEvent("onload",K))}function K(){(z.addEventListener||"load"===event.type||"complete"===z.readyState)&&(J(),n.ready())}n.ready.promise=function(b){if(!I)if(I=n.Deferred(),"complete"===z.readyState)setTimeout(n.ready);else if(z.addEventListener)z.addEventListener("DOMContentLoaded",K,!1),a.addEventListener("load",K,!1);else{z.attachEvent("onreadystatechange",K),a.attachEvent("onload",K);var c=!1;try{c=null==a.frameElement&&z.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!n.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}J(),n.ready()}}()}return I.promise(b)};var L="undefined",M;for(M in n(l))break;l.ownLast="0"!==M,l.inlineBlockNeedsLayout=!1,n(function(){var a,b,c=z.getElementsByTagName("body")[0];c&&(a=z.createElement("div"),a.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",b=z.createElement("div"),c.appendChild(a).appendChild(b),typeof b.style.zoom!==L&&(b.style.cssText="border:0;margin:0;width:1px;padding:1px;display:inline;zoom:1",(l.inlineBlockNeedsLayout=3===b.offsetWidth)&&(c.style.zoom=1)),c.removeChild(a),a=b=null)}),function(){var a=z.createElement("div");if(null==l.deleteExpando){l.deleteExpando=!0;try{delete a.test}catch(b){l.deleteExpando=!1}}a=null}(),n.acceptData=function(a){var b=n.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(O,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}n.data(a,b,c)}else c=void 0}return c}function Q(a){var b;for(b in a)if(("data"!==b||!n.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function R(a,b,d,e){if(n.acceptData(a)){var f,g,h=n.expando,i=a.nodeType,j=i?n.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||n.guid++:h),j[k]||(j[k]=i?{}:{toJSON:n.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=n.extend(j[k],b):j[k].data=n.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[n.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[n.camelCase(b)])):f=g,f +}}function S(a,b,c){if(n.acceptData(a)){var d,e,f=a.nodeType,g=f?n.cache:a,h=f?a[n.expando]:n.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){n.isArray(b)?b=b.concat(n.map(b,n.camelCase)):b in d?b=[b]:(b=n.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!Q(d):!n.isEmptyObject(d))return}(c||(delete g[h].data,Q(g[h])))&&(f?n.cleanData([a],!0):l.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}n.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?n.cache[a[n.expando]]:a[n.expando],!!a&&!Q(a)},data:function(a,b,c){return R(a,b,c)},removeData:function(a,b){return S(a,b)},_data:function(a,b,c){return R(a,b,c,!0)},_removeData:function(a,b){return S(a,b,!0)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=n.data(f),1===f.nodeType&&!n._data(f,"parsedAttrs"))){c=g.length;while(c--)d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d]));n._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){n.data(this,a)}):arguments.length>1?this.each(function(){n.data(this,a,b)}):f?P(f,a,n.data(f,a)):void 0},removeData:function(a){return this.each(function(){n.removeData(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=n._data(a,b),c&&(!d||n.isArray(c)?d=n._data(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return n._data(a,c)||n._data(a,c,{empty:n.Callbacks("once memory").add(function(){n._removeData(a,b+"queue"),n._removeData(a,c)})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},X=/^(?:checkbox|radio)$/i;!function(){var a=z.createDocumentFragment(),b=z.createElement("div"),c=z.createElement("input");if(b.setAttribute("className","t"),b.innerHTML="
            a",l.leadingWhitespace=3===b.firstChild.nodeType,l.tbody=!b.getElementsByTagName("tbody").length,l.htmlSerialize=!!b.getElementsByTagName("link").length,l.html5Clone="<:nav>"!==z.createElement("nav").cloneNode(!0).outerHTML,c.type="checkbox",c.checked=!0,a.appendChild(c),l.appendChecked=c.checked,b.innerHTML="",l.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,a.appendChild(b),b.innerHTML="",l.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,l.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){l.noCloneEvent=!1}),b.cloneNode(!0).click()),null==l.deleteExpando){l.deleteExpando=!0;try{delete b.test}catch(d){l.deleteExpando=!1}}a=b=c=null}(),function(){var b,c,d=z.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(l[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),l[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var Y=/^(?:input|select|textarea)$/i,Z=/^key/,$=/^(?:mouse|contextmenu)|click/,_=/^(?:focusinfocus|focusoutblur)$/,ab=/^([^.]*)(?:\.(.+)|)$/;function bb(){return!0}function cb(){return!1}function db(){try{return z.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=n.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return typeof n===L||a&&n.event.triggered===a.type?void 0:n.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(F)||[""],h=b.length;while(h--)f=ab.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=n.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=n.event.special[o]||{},l=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},i),(m=g[o])||(m=g[o]=[],m.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,l):m.push(l),n.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n.hasData(a)&&n._data(a);if(r&&(k=r.events)){b=(b||"").match(F)||[""],j=b.length;while(j--)if(h=ab.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=m.length;while(f--)g=m[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(m.splice(f,1),g.selector&&m.delegateCount--,l.remove&&l.remove.call(a,g));i&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(k)&&(delete r.handle,n._removeData(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,m,o=[d||z],p=j.call(b,"type")?b.type:b,q=j.call(b,"namespace")?b.namespace.split("."):[];if(h=l=d=d||z,3!==d.nodeType&&8!==d.nodeType&&!_.test(p+n.event.triggered)&&(p.indexOf(".")>=0&&(q=p.split("."),p=q.shift(),q.sort()),g=p.indexOf(":")<0&&"on"+p,b=b[n.expando]?b:new n.Event(p,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=q.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),k=n.event.special[p]||{},e||!k.trigger||k.trigger.apply(d,c)!==!1)){if(!e&&!k.noBubble&&!n.isWindow(d)){for(i=k.delegateType||p,_.test(i+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),l=h;l===(d.ownerDocument||z)&&o.push(l.defaultView||l.parentWindow||a)}m=0;while((h=o[m++])&&!b.isPropagationStopped())b.type=m>1?i:k.bindType||p,f=(n._data(h,"events")||{})[b.type]&&n._data(h,"handle"),f&&f.apply(h,c),f=g&&h[g],f&&f.apply&&n.acceptData(h)&&(b.result=f.apply(h,c),b.result===!1&&b.preventDefault());if(b.type=p,!e&&!b.isDefaultPrevented()&&(!k._default||k._default.apply(o.pop(),c)===!1)&&n.acceptData(d)&&g&&d[p]&&!n.isWindow(d)){l=d[g],l&&(d[g]=null),n.event.triggered=p;try{d[p]()}catch(r){}n.event.triggered=void 0,l&&(d[g]=l)}return b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(n._data(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,g=0;while((e=f.handlers[g++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(e.namespace))&&(a.handleObj=e,a.data=e.data,c=((n.event.special[e.origType]||{}).handle||e.handler).apply(f.elem,i),void 0!==c&&(a.result=c)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(e=[],f=0;h>f;f++)d=b[f],c=d.selector+" ",void 0===e[c]&&(e[c]=d.needsContext?n(c,this).index(i)>=0:n.find(c,this,null,[i]).length),e[c]&&e.push(d);e.length&&g.push({elem:i,handlers:e})}return h]","i"),ib=/^\s+/,jb=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,kb=/<([\w:]+)/,lb=/\s*$/g,sb={option:[1,""],legend:[1,"
            ","
            "],area:[1,"",""],param:[1,"",""],thead:[1,"","
            "],tr:[2,"","
            "],col:[2,"","
            "],td:[3,"","
            "],_default:l.htmlSerialize?[0,"",""]:[1,"X
            ","
            "]},tb=eb(z),ub=tb.appendChild(z.createElement("div"));sb.optgroup=sb.option,sb.tbody=sb.tfoot=sb.colgroup=sb.caption=sb.thead,sb.th=sb.td;function vb(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==L?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==L?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||n.nodeName(d,b)?f.push(d):n.merge(f,vb(d,b));return void 0===b||b&&n.nodeName(a,b)?n.merge([a],f):f}function wb(a){X.test(a.type)&&(a.defaultChecked=a.checked)}function xb(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function yb(a){return a.type=(null!==n.find.attr(a,"type"))+"/"+a.type,a}function zb(a){var b=qb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ab(a,b){for(var c,d=0;null!=(c=a[d]);d++)n._data(c,"globalEval",!b||n._data(b[d],"globalEval"))}function Bb(a,b){if(1===b.nodeType&&n.hasData(a)){var c,d,e,f=n._data(a),g=n._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)n.event.add(b,c,h[c][d])}g.data&&(g.data=n.extend({},g.data))}}function Cb(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!l.noCloneEvent&&b[n.expando]){e=n._data(b);for(d in e.events)n.removeEvent(b,d,e.handle);b.removeAttribute(n.expando)}"script"===c&&b.text!==a.text?(yb(b).text=a.text,zb(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),l.html5Clone&&a.innerHTML&&!n.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&X.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}n.extend({clone:function(a,b,c){var d,e,f,g,h,i=n.contains(a.ownerDocument,a);if(l.html5Clone||n.isXMLDoc(a)||!hb.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(ub.innerHTML=a.outerHTML,ub.removeChild(f=ub.firstChild)),!(l.noCloneEvent&&l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(d=vb(f),h=vb(a),g=0;null!=(e=h[g]);++g)d[g]&&Cb(e,d[g]);if(b)if(c)for(h=h||vb(a),d=d||vb(f),g=0;null!=(e=h[g]);g++)Bb(e,d[g]);else Bb(a,f);return d=vb(f,"script"),d.length>0&&Ab(d,!i&&vb(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k,m=a.length,o=eb(b),p=[],q=0;m>q;q++)if(f=a[q],f||0===f)if("object"===n.type(f))n.merge(p,f.nodeType?[f]:f);else if(mb.test(f)){h=h||o.appendChild(b.createElement("div")),i=(kb.exec(f)||["",""])[1].toLowerCase(),k=sb[i]||sb._default,h.innerHTML=k[1]+f.replace(jb,"<$1>")+k[2],e=k[0];while(e--)h=h.lastChild;if(!l.leadingWhitespace&&ib.test(f)&&p.push(b.createTextNode(ib.exec(f)[0])),!l.tbody){f="table"!==i||lb.test(f)?""!==k[1]||lb.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)n.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}n.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),l.appendChecked||n.grep(vb(p,"input"),wb),q=0;while(f=p[q++])if((!d||-1===n.inArray(f,d))&&(g=n.contains(f.ownerDocument,f),h=vb(o.appendChild(f),"script"),g&&Ab(h),c)){e=0;while(f=h[e++])pb.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=n.expando,j=n.cache,k=l.deleteExpando,m=n.event.special;null!=(d=a[h]);h++)if((b||n.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)m[e]?n.event.remove(d,e):n.removeEvent(d,e,g.handle);j[f]&&(delete j[f],k?delete d[i]:typeof d.removeAttribute!==L?d.removeAttribute(i):d[i]=null,c.push(f))}}}),n.fn.extend({text:function(a){return W(this,function(a){return void 0===a?n.text(this):this.empty().append((this[0]&&this[0].ownerDocument||z).createTextNode(a))},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=xb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=xb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(vb(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&Ab(vb(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&n.cleanData(vb(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&n.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return W(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(gb,""):void 0;if(!("string"!=typeof a||nb.test(a)||!l.htmlSerialize&&hb.test(a)||!l.leadingWhitespace&&ib.test(a)||sb[(kb.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(jb,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(vb(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(vb(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,k=this.length,m=this,o=k-1,p=a[0],q=n.isFunction(p);if(q||k>1&&"string"==typeof p&&!l.checkClone&&ob.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(k&&(i=n.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=n.map(vb(i,"script"),yb),f=g.length;k>j;j++)d=i,j!==o&&(d=n.clone(d,!0,!0),f&&n.merge(g,vb(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,n.map(g,zb),j=0;f>j;j++)d=g[j],pb.test(d.type||"")&&!n._data(d,"globalEval")&&n.contains(h,d)&&(d.src?n._evalUrl&&n._evalUrl(d.src):n.globalEval((d.text||d.textContent||d.innerHTML||"").replace(rb,"")));i=c=null}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=0,e=[],g=n(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),n(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Db,Eb={};function Fb(b,c){var d=n(c.createElement(b)).appendTo(c.body),e=a.getDefaultComputedStyle?a.getDefaultComputedStyle(d[0]).display:n.css(d[0],"display");return d.detach(),e}function Gb(a){var b=z,c=Eb[a];return c||(c=Fb(a,b),"none"!==c&&c||(Db=(Db||n("