Notes: Add tasks, sort

This commit is contained in:
Adam Porter 2020-03-20 15:59:28 -05:00
parent d55c01d2ae
commit b3ee9a9c08

720
notes.org
View file

@ -2,6 +2,10 @@
* Tasks * Tasks
** TODO [#A] Add ~:auto~ keyword to ~(planning)~ predicate
It should act like ~(or (deadline auto) (scheduled :to today))~.
** TODO [#A] Document sorters ** 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. 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.
@ -28,9 +32,7 @@ Also, should support an ~id~ one.
+ Added example to =examples.org=. + Added example to =examples.org=.
*** DONE Save query from ql-agenda buffer *** TODO Bookmarks
*** DONE Access saved query from saved query list
*** TODO Org link types *** TODO Org link types
:PROPERTIES: :PROPERTIES:
@ -43,7 +45,265 @@ This would be useful for having a menu of saved queries as Org links, or even bo
**** TODO For saved queries **** TODO For saved queries
*** TODO Bookmarks *** DONE Access saved query from saved query list
*** DONE Save query from ql-agenda buffer
** UNDERWAY [#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.
** 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!
** TODO [#B] Add more sorters?
+ [ ] =category=
+ [ ] Any date :: e.g. it would search for timestamps (active/inactive?) anywhere in an entry
** 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] Default sort
Would probably be useful to have a default sort option.
** TODO [#B] 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 [#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.
** TODO [#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
** TODO [#B] Timeline view ** TODO [#B] Timeline view
@ -186,104 +446,6 @@ Another, more up-to-date implementation:
;; (org-ql-view-timeline "~/org/main.org" :from "2019-11-01") ;; (org-ql-view-timeline "~/org/main.org" :from "2019-11-01")
#+END_SRC #+END_SRC
** 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] 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 [#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.
** TODO [#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
** TODO [#B] Timeline view ** TODO [#B] Timeline view
:PROPERTIES: :PROPERTIES:
:ID: 00573552-ffe9-4608-8904-7f6c73246b6d :ID: 00573552-ffe9-4608-8904-7f6c73246b6d
@ -363,137 +525,6 @@ e.g. as mentioned by Samuel Wales at https://lists.gnu.org/archive/html/emacs-or
e.g. doesn't currently show the =View= header. e.g. doesn't currently show the =View= header.
** 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] ~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] 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
[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.
** 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.
*** TODO 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.
*** Prior art
**** magit-section
**** [[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.
*** 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.
** UNDERWAY [#B] "Node" caching ** 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. [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,6 +629,49 @@ This works okay (except the priority accessor needs to be fixed, because Org pri
:action #'helm-org-goto-marker)))) :action #'helm-org-goto-marker))))
#+END_SRC #+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.
*** TODO 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 Fancier plain query syntax for tags and properties ** MAYBE Fancier plain query syntax for tags and properties
e.g. something like [[https://200ok.ch/posts/2020-02-09_creating_org_mode_sparse_trees_in_emacs_and_organice.html][Organice has now]]. e.g. something like [[https://200ok.ch/posts/2020-02-09_creating_org_mode_sparse_trees_in_emacs_and_organice.html][Organice has now]].
@ -617,133 +691,10 @@ Searching that with a query like =property:author=Fabrice= returns nothing; the
[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. [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.
** 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.
** MAYBE Use Bovine or Wisent to parse non-sexp syntax ** MAYBE Use Bovine or Wisent to parse non-sexp syntax
They're both built-in to Emacs, so we could drop the =peg= dependency. They're both built-in to Emacs, so we could drop the =peg= dependency.
** 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 Byte-compile lambdas ** DONE Byte-compile lambdas
CLOSED: [2018-05-09 Wed 17:30] CLOSED: [2018-05-09 Wed 17:30]
:LOGBOOK: :LOGBOOK:
@ -982,6 +933,63 @@ 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. Also, maybe instead of having a single =date= selector, I should have =scheduled=, =deadline=, etc.
** 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] 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] 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
** 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.
* References * References
** [[https://m00natic.github.io/lisp/manual-jit.html][Uniform Structured Syntax, Metaprogramming and Run-time Compilation]] ** [[https://m00natic.github.io/lisp/manual-jit.html][Uniform Structured Syntax, Metaprogramming and Run-time Compilation]]