diff --git a/README.org b/README.org index ad6d25f..32dada2 100644 --- a/README.org +++ b/README.org @@ -73,6 +73,13 @@ More examples are available in [[examples.org]]. (tags "Emacs") (priority "A"))) (agenda))))) + + ;; Show a "stuck projects" view: tasks that are not done and have only + ;; non-task children. + (org-ql-agenda (org-agenda-files) + (and (todo) + (children) + (not (children (todo))))) #+END_SRC * Usage @@ -136,10 +143,12 @@ A query is a lisp form which may contain arbitrary lisp forms, as well as certai Arguments are listed next to predicate names, where applicable. + ~category (&optional categories)~ :: Return non-nil if current heading is in one or more of ~CATEGORIES~ (a list of strings). ++ ~children (&optional query)~ :: Return non-nil if current heading has direct child headings. If ~QUERY~, test it against child headings. This selector may be nested, e.g. to match grandchild headings. + ~clocked (&key from to on)~ :: Return non-nil if current entry was clocked in given period. If no arguments are specified, return non-nil if entry was clocked at any time. If ~FROM~, return non-nil if entry was clocked on or after ~FROM~. If ~TO~, return non-nil if entry was clocked on or before ~TO~. If ~ON~, return non-nil if entry was clocked on date ~ON~. ~FROM~, ~TO~, and ~ON~ should be strings parseable by ~parse-time-string~ but may omit the time value. Note: Clock entries are expected to be clocked out. Currently clocked entries (i.e. with unclosed timestamp ranges) are ignored. + ~closed (&optional comparator target-date)~ :: Return non-nil if entry's closed date compares with ~TARGET-DATE~ using ~COMPARATOR~. ~TARGET-DATE~ should be a string parseable by ~date-to-day~. ~COMPARATOR~ should be a function (like ~<=~). + ~date (&optional comparator target-date type)~ :: Return non-nil if Org entry at point has date of ~TYPE~ that compares with ~TARGET-DATE~ using ~COMPARATOR~. Checks all Org-formatted timestamp strings in entry. ~TYPE~ may be ~active~, ~inactive~, or ~all~, to control whether active, inactive, or all timestamps are checked. Ranges of each type are also checked. ~TARGET-DATE~ should be a string parseable by ~date-to-day~. ~COMPARATOR~ should be a function (like ~<=~). + ~deadline (&optional comparator target-date)~ :: Return non-nil if entry's deadline compares with ~TARGET-DATE~ using ~COMPARATOR~. ~TARGET-DATE~ should be a string parseable by ~date-to-day~; or if omitted, it is determined automatically using ~org-deadline-warning-days~. ~COMPARATOR~ should be a function (like ~<=~). ++ ~descendants (&optional query)~ :: Return non-nil if current heading has descendant headings. If ~QUERY~, test it against descendant headings. This selector may be nested (if you can grok the nesting!). + ~done~ :: Return non-nil if entry's ~TODO~ keyword is in ~org-done-keywords~. + ~habit~ :: Return non-nil if entry is a habit. + ~heading (regexp)~ :: Return non-nil if current entry's heading matches ~REGEXP~ (a regexp string). @@ -366,6 +375,7 @@ Expands into a call to ~org-ql-select~ with the same arguments. For convenience + Selectors ~ts-a~ and ~ts-i~, aliases for ~ts-active~ and ~ts-inactive~. + Selector ~ts~ now accepts a ~:type~ argument. + Face =org-ql-agenda-due-date=. ++ Selectors ~(children)~ and ~(descendants)~. *Changed* + Function ~org-ql-query~ renamed to ~org-ql-select~. ~org-ql-query~ now refers to a new function. diff --git a/org-ql.el b/org-ql.el index 591031e..377db58 100644 --- a/org-ql.el +++ b/org-ql.el @@ -253,6 +253,11 @@ Replaces bare strings with (regexp) selectors, and appropriate ,@(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))) (`(,(or 'ts-active 'ts-a) . ,rest) `(ts :type active ,@rest)) (`(,(or 'ts-inactive 'ts-i) . ,rest) `(ts :type inactive ,@rest)) (_ element)))) @@ -549,6 +554,32 @@ empty time values to 23:59:59; otherwise, to 00:00:00." ;;;;; Predicates +(org-ql--defpred children (query) + "Return non-nil if current entry has children matching QUERY." + (save-excursion + (save-restriction + (org-narrow-to-subtree) + (when (org-goto-first-child) + ;; Lisp makes this easy and elegant: all we do is modify the query, + ;; nesting it inside an (and), and it doesn't descend into grandchildren. + (let* ((level (org-current-level)) + (query (cl-typecase query + (byte-code-function `(and (level ,level) + (funcall ,query))) + (t `(and (level ,level) + ,query))))) + (org-ql-select (current-buffer) + query :narrow t :action (lambda () t))))))) + +(org-ql--defpred descendants (query) + "Return non-nil if current entry has descendants matching QUERY." + (save-excursion + (save-restriction + (org-narrow-to-subtree) + (when (org-goto-first-child) + (org-ql-select (current-buffer) + query :narrow t :action (lambda () t)))))) + (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. diff --git a/tests/test-org-ql.el b/tests/test-org-ql.el index a97d875..245aa2c 100644 --- a/tests/test-org-ql.el +++ b/tests/test-org-ql.el @@ -219,6 +219,29 @@ RESULTS should be a list of strings as returned by (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)) + '("Test data" "Take over the universe" "Take over the world" "Take over Mars" "Take over the moon" "Recurring" "Ideas" "Code" "Misc"))) + (org-ql-it "with sub-query" + (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"))) + '("Test data" "Take over the universe")))) + + (describe "(descendants)" + (org-ql-it "without arguments" + (org-ql-expect ((descendants)) + '("Test data" "Take over the universe" "Take over the world" "Take over Mars" "Take over the moon" "Recurring" "Ideas" "Code" "Misc"))) + (org-ql-it "with sub-query" + (org-ql-expect ((descendants (todo "CHECK"))) + '("Test data" "Recurring"))) + (org-ql-it "with granddescendants query" + (org-ql-expect ((descendants (descendants "moon"))) + '("Test data" "Take over the universe" "Take over the moon" "Code")))) + (describe "(clocked)" (org-ql-it "without arguments"