branch: master commit 9a38d60fbdda634ebf063f8e9888ab57bc582aa8 Author: Ian D <du...@gnu.org> Commit: Ian D <du...@gnu.org>
Fixed chain-find and planning actions * org-edna.el (org-edna-finder/chain-find): Fixed to match org-depend.el (org-edna--handle-planning): Fix to recognize when a time was used. * org-edna.org: Documented planning actions. * org-edna-tests.el: Added tests for planning. * org-edna-tests.org: Added planning test entry. --- org-edna-tests.el | 64 ++++++++++++++++++++++ org-edna-tests.org | 5 ++ org-edna.el | 40 ++++++++++---- org-edna.org | 153 +++++++++++++++++++++++++++++++++++++++++++++-------- 4 files changed, 228 insertions(+), 34 deletions(-) diff --git a/org-edna-tests.el b/org-edna-tests.el index 2a523c9..14c0caa 100644 --- a/org-edna-tests.el +++ b/org-edna-tests.el @@ -108,6 +108,10 @@ (defconst org-edna-test-file (expand-file-name "org-edna-tests.org" org-edna-test-dir)) +;; Jan 15, 2000; chosen at random +(defconst org-edna-test-time + (encode-time 0 0 0 15 1 2000)) + ;; Finders @@ -161,8 +165,68 @@ (org-edna-action/todo nil "DONE") (should (string-equal (org-entry-get nil "TODO") "DONE")) (org-edna-action/todo nil "TODO") + (should (string-equal (org-entry-get nil "TODO") "TODO")) + (org-edna-action/todo nil 'DONE) + (should (string-equal (org-entry-get nil "TODO") "DONE")) + (org-edna-action/todo nil 'TODO) (should (string-equal (org-entry-get nil "TODO") "TODO"))))) +(ert-deftest org-edna-action-scheduled/wkdy () + ;; Override `current-time' so we can get a deterministic value + (cl-letf* (((symbol-function 'current-time) (lambda () org-edna-test-time)) + (org-agenda-files `(,org-edna-test-file)) + (target (org-id-find "0d491588-7da3-43c5-b51a-87fbd34f79f7" t))) + (org-with-point-at target + (org-edna-action/scheduled nil "Mon") + (should (string-equal (org-entry-get nil "SCHEDULED") + "<2000-01-17 Mon>")) + (org-edna-action/scheduled nil 'rm) + (should (not (org-entry-get nil "SCHEDULED"))) + (org-edna-action/scheduled nil "Mon 9:00") + (should (string-equal (org-entry-get nil "SCHEDULED") + "<2000-01-17 Mon 09:00>")) + (org-edna-action/scheduled nil 'rm) + (should (not (org-entry-get nil "SCHEDULED")))))) + +(ert-deftest org-edna-action-scheduled/cp () + ;; Override `current-time' so we can get a deterministic value + (let* ((org-agenda-files `(,org-edna-test-file)) + (target (org-id-find "0d491588-7da3-43c5-b51a-87fbd34f79f7" t)) + (source (org-id-find "97e6b0f0-40c4-464f-b760-6e5ca9744eb5" t)) + (pairs '((cp . rm) (copy . remove) ("cp" . "rm") ("copy" . "remove")))) + (org-with-point-at target + (dolist (pair pairs) + (message "Pair: %s" pair) + (org-edna-action/scheduled source (car pair)) + (should (string-equal (org-entry-get nil "SCHEDULED") + "<2000-01-15 Sat 00:00>")) + (org-edna-action/scheduled source (cdr pair)) + (should (not (org-entry-get nil "SCHEDULED"))))))) + +(ert-deftest org-edna-action-scheduled/inc () + ;; Override `current-time' so we can get a deterministic value + (cl-letf* (((symbol-function 'current-time) (lambda () org-edna-test-time)) + (org-agenda-files `(,org-edna-test-file)) + (target (org-id-find "97e6b0f0-40c4-464f-b760-6e5ca9744eb5" t))) + (org-with-point-at target + ;; Time started at Jan 15, 2000 + ;; Increment 1 minute + (org-edna-action/scheduled nil "+1M") + (should (string-equal (org-entry-get nil "SCHEDULED") + "<2000-01-15 Sat 00:01>")) + (org-edna-action/scheduled nil "-1M") + (should (string-equal (org-entry-get nil "SCHEDULED") + "<2000-01-15 Sat 00:00>")) + (org-edna-action/scheduled nil "+1d") + (should (string-equal (org-entry-get nil "SCHEDULED") + "<2000-01-16 Sun 00:00>")) + (org-edna-action/scheduled nil "++1h") + (should (string-equal (org-entry-get nil "SCHEDULED") + "<2000-01-15 Sat 01:00>")) + (org-edna-action/scheduled nil "2000-01-15 Sat 00:00") + (should (string-equal (org-entry-get nil "SCHEDULED") + "<2000-01-15 Sat 00:00>"))))) + ;; Conditions diff --git a/org-edna-tests.org b/org-edna-tests.org index 6f3341c..666ec1e 100644 --- a/org-edna-tests.org +++ b/org-edna-tests.org @@ -28,6 +28,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. :ID: b010cbad-60dc-46ef-a164-eb155e62cbb2 :LOGGING: nil :END: +** TODO ID Heading 3 +SCHEDULED: <2000-01-15 Sat 00:00> +:PROPERTIES: +:ID: 97e6b0f0-40c4-464f-b760-6e5ca9744eb5 +:END: ** Scheduled Headings *** TODO Scheduled Heading 1 SCHEDULED: <2017-01-01 Sun> diff --git a/org-edna.el b/org-edna.el index 73d2bd0..da8747a 100644 --- a/org-edna.el +++ b/org-edna.el @@ -360,9 +360,15 @@ IDS are all UUIDs as understood by `org-id-find'." (defun org-edna-finder/chain-find (&rest options) ;; sortfun - function to use to sort elements - ;; filterufn - Function to use to filter elements + ;; filterfun - Function to use to filter elements ;; Both should handle positioning point - (let (targets sortfun filterfun) + (let (targets + sortfun + ;; From org-depend.el: + ;; (and (not todo-and-done-only) + ;; (member (second item) org-done-keywords)) + (filterfun (lambda (target) + (member (org-entry-get target "TODO") org-done-keywords)))) (dolist (opt options) (pcase opt ('from-top @@ -374,13 +380,17 @@ IDS are all UUIDs as understood by `org-id-find'." ('no-wrap (setq targets (org-edna-finder/rest-of-siblings))) ('todo-only + ;; Remove any entry without a TODO keyword, or with a DONE keyword (setq filterfun (lambda (target) - (org-entry-get target "TODO")))) + (let ((kwd (org-entry-get target "TODO"))) + (or (not kwd) + (member kwd org-done-keywords)))))) ('todo-and-done-only + ;; Remove any entry without a TODO keyword (setq filterfun (lambda (target) - (member (org-entry-get target "TODO") org-done-keywords)))) + (not (org-entry-get target "TODO"))))) ('priority-up (setq sortfun (lambda (lhs rhs) @@ -408,7 +418,7 @@ IDS are all UUIDs as understood by `org-id-find'." (when (and targets sortfun) (setq targets (seq-sort sortfun targets))) (when (and targets filterfun) - (setq targets (seq-filter filterfun targets))) + (setq targets (seq-remove filterfun targets))) (when targets (list (seq-elt 0 targets))))) @@ -446,24 +456,32 @@ IDS are all UUIDs as understood by `org-id-find'." ("h" . hour) ("M" . minute)))) (cond - ((member arg '('rm 'remove "rm" "remove")) + ((member arg '(rm remove "rm" "remove")) (org-add-planning-info nil nil type)) - ((member arg '('cp 'copy "cp" "copy")) + ((member arg '(cp copy "cp" "copy")) + (unless last-ts + (error "Tried to copy but last entry doesn't have a timestamp")) ;; Copy old time verbatim (org-add-planning-info type last-ts)) ((string-match-p "\\`[+-]" arg) + ;; Starts with a + or -, so assume we're incrementing a timestamp ;; We support hours and minutes, so this must be supported separately, ;; since org-read-date-analyze doesn't - ;; Starts with a + or -, so assume we're incrementing a timestamp (pcase-let* ((`(,n ,what-string ,def) (org-read-date-get-relative arg this-time current)) (ts (if def current-ts this-ts)) (what (cdr (assoc-string what-string type-map)))) (org--deadline-or-schedule nil type (org-edna--mod-timestamp ts n what)))) (t ;; For everything else, assume `org-read-date-analyze' can handle it - (let* ((parsed-time (org-read-date-analyze arg this-time (decode-time this-time))) - (final-time (apply 'encode-time parsed-time)) - (new-ts (format-time-string "%F %R" final-time))) + + ;; The third argument to `org-read-date-analyze' specifies the defaults to + ;; use if that time component isn't specified. Since there's no way to + ;; tell if a time was specified, tell `org-read-date-analyze' to use nil + ;; if no time is found. + (let* ((parsed-time (org-read-date-analyze arg this-time '(nil nil nil nil nil nil))) + (have-time (nth 2 parsed-time)) + (final-time (apply 'encode-time (mapcar (lambda (e) (or e 0)) parsed-time))) + (new-ts (format-time-string (if have-time "%F %R" "%F") final-time))) (org--deadline-or-schedule nil type new-ts)))))) (defun org-edna-action/scheduled (last-entry &rest args) diff --git a/org-edna.org b/org-edna.org index d225adb..5144f5a 100644 --- a/org-edna.org +++ b/org-edna.org @@ -118,17 +118,19 @@ scheduling another task, marking another task as TODO, or renaming a file. :END: #+cindex: syntax -The basic syntax of Edna's commands is KEYWORD(ARG1,ARG2,...) +Edna has its own language for commands, the basic form of which is KEYWORD(ARG1 ARG2 ...) -KEYWORD can be any valid symbol, such as key-word, KEY_WORD, or keyword?. +KEYWORD can be any valid lisp symbol, such as key-word, KEY_WORD, or keyword?. Each argument can be one of the following: -- A symbol, such as arg or arg-1 -- A valid lisp form, such as (+ 1 2) or (or a b) +- A symbol, such as arg or org-mode - A quoted string, such as "hello" or "My name is Edna" +- A number, such as 0.5, +1e3, or -5 +- A UUID, such as c5e30c76-879a-494d-9281-3a4b559c1a3c -Any quotes should be escaped (e.g. "\"\""). +Each argument takes specific datatypes as input, so be sure to read the entry +before using it. The parentheses can be omitted for commands with no arguments. * Basic Features @@ -165,6 +167,42 @@ For example: In the above example, "Heading 5" will be blocked until "Heading 1", "Heading 3", and "Heading 4" are marked "DONE", while "Heading 2" is ignored. +*** chain-find + +chain-find(OPTION OPTION...) + +Identical to the chain argument in org-depend, chain-find selects its single +target using the following method: + +1. Creates a list of possible targets +2. Filters the targets from Step 1 +3. Sorts the targets from Step 2 + +After this is finished, chain-find selects the first target in the list and +returns it. + +One option from each of the following three categories may be used; if more than +one is specified, the last will be used. + +<<Selection>> + +- from-top: Select siblings of the current headline, starting at the top +- from-bottom: As above, but from the bottom +- from-current: Selects siblings, starting from the headline (wraps) +- no-wrap: As above, but without wrapping + +<<Filtering>> + +- todo-only: Select only targets with TODO state set that isn't a DONE keyword +- todo-and-done-only: Select all targets with a TODO state set + +<<Sorting>> + +- priority-up: Sort by priority, highest first +- priority-down: Same, but lowest first +- effort-up: Sort by effort, highest first +- effort-down: Sort by effort, lowest first + *** children :PROPERTIES: :DESCRIPTION: Find all immediate children @@ -176,7 +214,7 @@ headline. In order to get all levels of children of the current headline, use the [[#descendants][descendants]] keyword instead. -*** TODO descendants +*** descendants :PROPERTIES: :DESCRIPTION: Find all descendants :CUSTOM_ID: descendants @@ -193,8 +231,10 @@ EXAMPLE HERE :DESCRIPTION: Find a file by name :END: -The ~file~ finder finds a single file. The returned target will be the minimum -point in the file. +file(FILE) + +The ~file~ finder finds a single file, specified as a string. The returned target +will be the minimum point in the file. Note that with the default condition, ~file~ won't work. See [[#conditions][conditions]] for how to set a different condition. For example: @@ -208,9 +248,6 @@ to set a different condition. For example: Here, "Test" will block until myfile.org is clear of headlines. -WARNING: Make sure to quote the file name. If not, Edna will interpret it as -"myfile\\.org" and create that file. - *** first-child :PROPERTIES: :CUSTOM_ID: first-child @@ -231,7 +268,7 @@ number of UUIDs may be specified. For example: #+BEGIN_EXAMPLE ,* TODO Test :PROPERTIES: - :BLOCKER: ids(62209a9a-c63b-45ef-b8a8-12e47a9ceed9,6dbd7921-a25c-4e20-b035-365677e00f30) + :BLOCKER: ids(62209a9a-c63b-45ef-b8a8-12e47a9ceed9 6dbd7921-a25c-4e20-b035-365677e00f30) :END: #+END_EXAMPLE @@ -239,19 +276,23 @@ Here, "Test" will block until the headline with ID 62209a9a-c63b-45ef-b8a8-12e47a9ceed9 and the headline with ID 6dbd7921-a25c-4e20-b035-365677e00f30 are set to "DONE". +Note that UUIDs need not be quoted; Edna will handle that for you. + *** match :PROPERTIES: :CUSTOM_ID: match :DESCRIPTION: Good old tag matching :END: +match(MATCH-STRING SCOPE SKIP) + The ~match~ keyword will take any arguments that ~org-map-entries~ usually takes. In fact, the arguments to ~match~ are passed straight into ~org-map-entries~. #+BEGIN_EXAMPLE ,* TODO Test :PROPERTIES: - :BLOCKER: match(test&mine,agenda) + :BLOCKER: match("test&mine" agenda) :END: #+END_EXAMPLE @@ -265,14 +306,46 @@ argument. :PROPERTIES: :CUSTOM_ID: next-sibling :END: + +The ~next-sibling~ keyword returns the next sibling of the current heading, if +any. + *** olp :PROPERTIES: :CUSTOM_ID: olp :END: + +olp(FILE OLP) + +Finds the heading given by OLP in FILE. Both arguments are strings. + +#+BEGIN_EXAMPLE +,* TODO Test + :PROPERTIES: + :BLOCKER: olp("test.org" "path/to/heading") + :END: +#+END_EXAMPLE + +"Test" will block if the heading "path/to/heading" in "test.org" is not DONE. + *** org-file :PROPERTIES: :CUSTOM_ID: org-file :END: + +org-file("FILE") + +A special form of ~file~, ~org-file~ will find FILE in ~org-directory~. + +#+BEGIN_EXAMPLE +,* TODO Test + :PROPERTIES: + :BLOCKER: org-file("test.org") + :END: +#+END_EXAMPLE + +Note that the file still requires an extension. + *** parent :PROPERTIES: :CUSTOM_ID: parent @@ -281,36 +354,70 @@ argument. :PROPERTIES: :CUSTOM_ID: previous-sibling :END: +*** rest-of-siblings + +Finds the remaining siblings on the same level as the current headline. + *** self :PROPERTIES: :CUSTOM_ID: self :END: + +Returns the current headline. + *** siblings :PROPERTIES: :CUSTOM_ID: siblings :END: +*** siblings-wrap + +Finds the siblings on the same level as the current headline, wrapping when it +reaches the end. + ** Actions Once Edna has collected its targets for a trigger, it will perform actions on them. *** Scheduled/Deadline -- PLANNING(WKDY[ TIME]) -> Set PLANNING to following weekday WKDY at TIME -- PLANNING(rm|remove) -> Remove PLANNING info -- PLANNING([copy|cp]) -> Copy timestamp verbatim -- PLANNING([+|-][+|-]NTHING) -> Increment(+) or decrement(-) source (double) or current (single) PLANNING by N THINGs +:PROPERTIES: +:CUSTOM_ID: planning +:END: + +scheduled(OPTIONS) +deadline(OPTIONS) + +There are several forms that the planning keywords can take: + +- PLANNING("WKDY[ TIME]") + + Sets PLANNING to the following weekday WKDY at TIME. If TIME is not + specified, only a date will be added to the target. + + WKDY is a weekday or weekday abbreviation (see ~org-read-date~) + + TIME is a time string HH:MM, etc. + +- PLANNING(rm|remove) + + Remove PLANNING from all targets. The argument to this form may be either a + string or a symbol. + +- PLANNING(copy|cp) -PLANNING is either scheduled or deadline + Copy PLANNING info verbatim from the current headline to all targets. The + argument to this form may be either a string or a symbol. -WKDY is a weekday or weekday abbreviation (see org-read-date) +- PLANNING("[+|-][+|-]NTHING") -TIME is a time string HH:MM, etc. + Increment(+) or decrement(-) source (double) or current (single) PLANNING by N + THINGs -N is an integer + N is an integer -THING is one of y (years), m (months), d (days), h (hours), or M (minutes) + THING is one of y (years), m (months), d (days), h (hours), or M (minutes) Examples: -scheduled(Mon 09:00) -> Set SCHEDULED to the following Monday at 9:00 +scheduled("Mon 09:00") -> Set SCHEDULED to the following Monday at 9:00 *** Todo State todo(NEW-STATE)