branch: master commit 9b8c043f80555447a935a585847bc5d3d7b37b72 Merge: 9a85042 556e03b Author: Ian Dunn <du...@gnu.org> Commit: Ian Dunn <du...@gnu.org>
Merge commit '556e03be1068d746e3d672185987c433302229fa' --- packages/org-edna/org-edna-tests.el | 277 +++++++++++++++++--- packages/org-edna/org-edna.el | 506 +++++++++++++++++++++++++++--------- packages/org-edna/org-edna.info | 379 ++++++++++++++++++++++----- packages/org-edna/org-edna.org | 143 +++++++++- 4 files changed, 1074 insertions(+), 231 deletions(-) diff --git a/packages/org-edna/org-edna-tests.el b/packages/org-edna/org-edna-tests.el index d3911db..a6b3554 100644 --- a/packages/org-edna/org-edna-tests.el +++ b/packages/org-edna/org-edna-tests.el @@ -70,89 +70,293 @@ (ert-deftest org-edna-parse-form-no-arguments () (let* ((input-string "test-string") - (parsed (org-edna-parse-form input-string))) + (parsed (org-edna-parse-string-form input-string))) (should parsed) - (should (= (length parsed) 4)) - (pcase-let* ((`(,token ,args ,modifier ,pos) parsed)) - (should (eq token 'test-string)) + (should (= (length parsed) 2)) + (pcase-let* ((`((,key . ,args) ,pos) parsed)) + (should (eq key 'test-string)) (should (not args)) - (should (not modifier)) (should (= pos 11))))) (ert-deftest org-edna-parse-form-no-arguments-modifier () (let* ((input-string "!test-string") - (parsed (org-edna-parse-form input-string))) + (parsed (org-edna-parse-string-form input-string))) (should parsed) - (should (= (length parsed) 4)) - (pcase-let* ((`(,token ,args ,modifier ,pos) parsed)) - (should (eq token 'test-string)) + (should (= (length parsed) 2)) + (pcase-let* ((`((,key . ,args) ,pos) parsed)) + (should (eq key '!test-string)) (should (not args)) - (should (eq modifier '!)) (should (= pos 12))))) (ert-deftest org-edna-parse-form-single-argument () (let* ((input-string "test-string(abc)") - (parsed (org-edna-parse-form input-string))) + (parsed (org-edna-parse-string-form input-string))) (should parsed) - (should (= (length parsed) 4)) - (pcase-let* ((`(,token ,args ,modifier ,pos) parsed)) - (should (eq token 'test-string)) + (should (= (length parsed) 2)) + (pcase-let* ((`((,key . ,args) ,pos) parsed)) + (should (eq key 'test-string)) (should (= (length args) 1)) (should (symbolp (nth 0 args))) (should (eq (nth 0 args) 'abc)) - (should (not modifier)) (should (= pos (length input-string)))))) (ert-deftest org-edna-parse-form-string-argument () (let* ((input-string "test-string(abc \"def (ghi)\")") - (parsed (org-edna-parse-form input-string))) + (parsed (org-edna-parse-string-form input-string))) (should parsed) - (should (= (length parsed) 4)) - (pcase-let* ((`(,token ,args ,modifier ,pos) parsed)) - (should (eq token 'test-string)) + (should (= (length parsed) 2)) + (pcase-let* ((`((,key . ,args) ,pos) parsed)) + (should (eq key 'test-string)) (should (= (length args) 2)) (should (symbolp (nth 0 args))) (should (eq (nth 0 args) 'abc)) (should (stringp (nth 1 args))) (should (string-equal (nth 1 args) "def (ghi)")) - (should (not modifier)) (should (= pos (length input-string)))))) (ert-deftest org-edna-parse-form-multiple-forms () (let ((input-string "test-string1 test-string2") pos) - (pcase-let* ((`(,token1 ,args1 ,modifier1 ,pos1) (org-edna-parse-form input-string))) - ;; (should (and token1 args1 modifier1 pos1)) - (should (eq token1 'test-string1)) + (pcase-let* ((`((,key1 . ,args1) ,pos1) (org-edna-parse-string-form input-string))) + (should (eq key1 'test-string1)) (should (not args1)) - (should (not modifier1)) (should (= pos1 13)) (setq pos pos1)) - (pcase-let* ((`(,token2 ,args2 ,modifier2 ,pos2) (org-edna-parse-form (substring input-string pos)))) - (should (eq token2 'test-string2)) + (pcase-let* ((`((,key2 . ,args2) ,pos2) (org-edna-parse-string-form (substring input-string pos)))) + (should (eq key2 'test-string2)) (should (not args2)) - (should (not modifier2)) (should (= pos2 12))))) (ert-deftest org-edna-parse-form-empty-argument-list () (let ((input-string "test-string1()")) - (pcase-let* ((`(,token1 ,args1 ,modifier1 ,pos1) (org-edna-parse-form input-string))) - (should (eq token1 'test-string1)) + (pcase-let* ((`((,key1 ,args1) ,pos1) (org-edna-parse-string-form input-string))) + (should (eq key1 'test-string1)) (should (not args1)) - (should (not modifier1)) (should (= pos1 (length input-string)))))) (ert-deftest org-edna-parse-form-condition () (let ((input-string "variable-set?()")) - (pcase-let* ((`(,token1 ,args1 ,modifier1 ,pos1) (org-edna-parse-form input-string)) - (`(,type . ,func) (org-edna--function-for-key token1))) - (should (eq token1 'variable-set?)) + (pcase-let* ((`((,key1 . ,args1) ,pos1) (org-edna-parse-string-form input-string)) + (`(,modifier1 . ,key1) (org-edna-break-modifier key1)) + (`(,type . ,func) (org-edna--function-for-key key1))) + (should (eq key1 'variable-set?)) (should (not args1)) (should (not modifier1)) (should (= pos1 (length input-string))) (should (eq type 'condition)) (should (eq func 'org-edna-condition/variable-set?))))) +(ert-deftest org-edna-form-to-sexp-no-arguments () + (let* ((input-string "self") + (sexp (org-edna-string-form-to-sexp-form input-string 'condition))) + (should (equal + sexp + '((self) + (!done?)))))) + +(ert-deftest org-edna-form-to-sexp-arguments () + (let* ((input-string "match(\"checklist\") todo!(TODO)") + (sexp (org-edna-string-form-to-sexp-form input-string 'action))) + (should (equal + sexp + '((match "checklist") + (todo! TODO)))))) + +(ert-deftest org-edna-form-to-sexp-if-no-else () + (let* ((input-string "if match(\"checklist\") done? then self todo!(TODO) endif") + (sexp (org-edna-string-form-to-sexp-form input-string 'action))) + (should (equal + sexp + '((if ((match "checklist") + (done?)) + ((self) + (todo! TODO)) + nil)))))) + +(ert-deftest org-edna-form-to-sexp-if-else () + (let* ((input-string "if match(\"checklist\") done? then self todo!(TODO) else siblings todo!(DONE) endif") + (sexp (org-edna-string-form-to-sexp-form input-string 'action))) + (should (equal + sexp + '((if ((match "checklist") + (done?)) + ((self) + (todo! TODO)) + ((siblings) + (todo! DONE)))))))) + +(ert-deftest org-edna-expand-sexp-form () + ;; Override cl-gentemp so we have a repeatable test + (cl-letf* (((symbol-function 'cl-gentemp) (lambda (&optional prefix) (intern (format "%s1" prefix)))) + (input-sexp '((self) + (!done?))) + (output-form (org-edna--expand-sexp-form input-sexp))) + (should (equal + output-form + '(let ((targets1 nil) + (consideration1 nil) + (blocking-entry1 nil)) + (setq targets1 (org-edna--add-targets targets1 (org-edna-finder/self))) + (setq blocking-entry1 + (or blocking-entry1 + (org-edna--handle-condition 'org-edna-condition/done? + '! 'nil targets1 + consideration1)))))))) + +(ert-deftest org-edna-expand-sexp-form-multiple () + (cl-letf* ((target-ctr 0) + (consideration-ctr 0) + (blocking-entry-ctr 0) + ((symbol-function 'cl-gentemp) + (lambda (&optional prefix) + (let ((ctr (pcase prefix + ("targets" (cl-incf target-ctr)) + ("consideration" (cl-incf consideration-ctr)) + ("blocking-entry" (cl-incf blocking-entry-ctr)) + (_ 0)))) + (intern (format "%s%s" prefix ctr))))) + (input-sexp '(((match "checklist") + (todo! DONE)) + ((siblings) + (todo! TODO)))) + (expected-form + '(let ((targets1 nil) + (consideration1 nil) + (blocking-entry1 nil)) + ;; Don't need a new set of variables + (progn + (setq targets1 + (org-edna--add-targets targets1 + (org-edna-finder/match "checklist"))) + (org-edna--handle-action 'org-edna-action/todo! + targets1 + (point-marker) + '(DONE))) + ;; No new set of variables here either + (progn + (setq targets1 + (org-edna--add-targets targets1 + (org-edna-finder/siblings))) + (org-edna--handle-action 'org-edna-action/todo! + targets1 + (point-marker) + '(TODO))))) + (output-form (org-edna--expand-sexp-form input-sexp))) + (should (equal output-form expected-form)))) + +(ert-deftest org-edna-expand-sexp-form-if-else () + (cl-letf* ((target-ctr 0) + (consideration-ctr 0) + (blocking-entry-ctr 0) + ((symbol-function 'cl-gentemp) + (lambda (&optional prefix) + (let ((ctr (pcase prefix + ("targets" (cl-incf target-ctr)) + ("consideration" (cl-incf consideration-ctr)) + ("blocking-entry" (cl-incf blocking-entry-ctr)) + (_ 0)))) + (intern (format "%s%s" prefix ctr))))) + (input-sexp '((if + ((match "checklist") + (done\?)) + ((self) + (todo! TODO)) + ((siblings) + (todo! DONE))))) + (expected-form '(let + ((targets1 nil) + (consideration1 nil) + (blocking-entry1 nil)) + (if + ;; No inheritance in the conditional scope + (not + (let + ((targets2 nil) + (consideration2 nil) + (blocking-entry2 nil)) + ;; Add targets for checklist match + (setq targets2 + (org-edna--add-targets targets2 + (org-edna-finder/match "checklist"))) + ;; Handle condition + (setq blocking-entry2 + (or blocking-entry2 + (org-edna--handle-condition 'org-edna-condition/done\? 'nil 'nil targets2 consideration2))))) + ;; Use the top-level scope for then case + (progn + ;; Add targets for self finder + (setq targets1 + (org-edna--add-targets targets1 + (org-edna-finder/self))) + ;; Mark as TODO + (org-edna--handle-action 'org-edna-action/todo! targets1 + (point-marker) + '(TODO))) + ;; Use the top-level scope for the else case + (progn + ;; Find siblings + (setq targets1 + (org-edna--add-targets targets1 + (org-edna-finder/siblings))) + ;; Mark as DONE + (org-edna--handle-action 'org-edna-action/todo! targets1 + (point-marker) + '(DONE)))))) + + (output-form (org-edna--expand-sexp-form input-sexp))) + (should (equal output-form expected-form)))) + +(ert-deftest org-edna-expand-sexp-form-if-no-else () + (cl-letf* ((target-ctr 0) + (consideration-ctr 0) + (blocking-entry-ctr 0) + ((symbol-function 'cl-gentemp) + (lambda (&optional prefix) + (let ((ctr (pcase prefix + ("targets" (cl-incf target-ctr)) + ("consideration" (cl-incf consideration-ctr)) + ("blocking-entry" (cl-incf blocking-entry-ctr)) + (_ 0)))) + (intern (format "%s%s" prefix ctr))))) + (input-sexp '((if + ((match "checklist") + (done\?)) + ((self) + (todo! TODO))))) + (expected-form '(let + ((targets1 nil) + (consideration1 nil) + (blocking-entry1 nil)) + (if + ;; No inheritance in the conditional scope + (not + (let + ((targets2 nil) + (consideration2 nil) + (blocking-entry2 nil)) + ;; Add targets for checklist match + (setq targets2 + (org-edna--add-targets targets2 + (org-edna-finder/match "checklist"))) + ;; Handle condition + (setq blocking-entry2 + (or blocking-entry2 + (org-edna--handle-condition 'org-edna-condition/done\? 'nil 'nil targets2 consideration2))))) + ;; Use the top-level scope for then case + (progn + ;; Add targets for self finder + (setq targets1 + (org-edna--add-targets targets1 + (org-edna-finder/self))) + ;; Mark as TODO + (org-edna--handle-action 'org-edna-action/todo! targets1 + (point-marker) + '(TODO))) + ;; End with a nil + nil))) + (output-form (org-edna--expand-sexp-form input-sexp))) + (should (equal output-form expected-form)))) + ;; Finders @@ -189,11 +393,12 @@ (ert-deftest org-edna-finder/match-blocker () (let* ((org-agenda-files `(,org-edna-test-file)) (heading (org-id-find "caccd0a6-d400-410a-9018-b0635b07a37e" t)) - (blocker (org-entry-get heading "BLOCKER"))) + (blocker (org-entry-get heading "BLOCKER")) + blocking-entry) (should (string-equal "match(\"test&1\")" blocker)) (org-with-point-at heading - (org-edna-process-form blocker 'condition)) - (should (string-equal (substring-no-properties org-block-entry-blocking) + (setq blocking-entry (org-edna-process-form blocker 'condition))) + (should (string-equal (substring-no-properties blocking-entry) "TODO Tagged Heading 1 :1:test:")))) (ert-deftest org-edna-finder/file () diff --git a/packages/org-edna/org-edna.el b/packages/org-edna/org-edna.el index f14bcc7..5f60fb9 100644 --- a/packages/org-edna/org-edna.el +++ b/packages/org-edna/org-edna.el @@ -7,7 +7,7 @@ ;; Keywords: convenience, text, org ;; URL: https://savannah.nongnu.org/projects/org-edna-el/ ;; Package-Requires: ((emacs "25.1") (seq "2.19") (org "9.0.5")) -;; Version: 1.0beta2 +;; Version: 1.0beta3 ;; This file is part of GNU Emacs. @@ -62,29 +62,40 @@ properties used during actions or conditions." :group 'org-edna :type 'boolean) -(defmacro org-edna--syntax-error (msg form pos) +;;; Form Parsing + +;; 3 types of "forms" here +;; +;; 1. String form; this is what you see in a BLOCKER or TRIGGER property +;; 2. Edna sexp form; this is the intermediary form, and form used in org-edna-form +;; 3. Lisp form; a form that can be evaluated by Emacs + +(defmacro org-edna--syntax-error (msg form error-form) "Signal an Edna syntax error. MSG will be reported to the user and should describe the error. FORM is the form that generated the error. -POS is the position in FORM at which the error occurred." - `(signal 'invalid-read-syntax (list :msg ,msg :form ,form :pos ,pos))) +ERROR-FORM is the sub-form in FORM at which the error occurred." + `(signal 'invalid-read-syntax (list :msg ,msg :form ,form :error-form ,error-form))) (defun org-edna--print-syntax-error (error-plist) "Prints the syntax error from ERROR-PLIST." - (let ((msg (plist-get error-plist :msg)) - (form (plist-get error-plist :form)) - (pos (plist-get error-plist :pos))) + (let* ((msg (plist-get error-plist :msg)) + (form (plist-get error-plist :form)) + (error-form (plist-get error-plist :error-form)) + (pos (string-match-p (symbol-name (car error-form)) form))) (message "Org Edna Syntax Error: %s\n%s\n%s" msg form (concat (make-string pos ?\ ) "^")))) (defun org-edna--transform-arg (arg) - "Transform ARG. + "Transform argument ARG. Currently, the following are handled: -- UUIDs (as determined by `org-uuidgen-p') are converted to strings" +- UUIDs (as determined by `org-uuidgen-p') are converted to strings + +Everything else is returned as is." (pcase arg ((and (pred symbolp) (let (pred org-uuidgen-p) (symbol-name arg))) @@ -92,44 +103,33 @@ Currently, the following are handled: (_ arg))) -(defun org-edna-parse-form (form &optional start) - "Parse Edna form FORM starting at position START." - (setq start (or start 0)) - (pcase-let* ((`(,token . ,pos) (read-from-string form start)) - (modifier nil) - (args nil)) - (unless token - (org-edna--syntax-error "Invalid Token" form start)) - ;; Check for either end of string or an opening parenthesis - (unless (or (equal pos (length form)) - (equal (string-match-p "\\s-" form pos) pos) - (equal (string-match-p "(" form pos) pos)) - (org-edna--syntax-error "Invalid character in form" form pos)) - ;; Parse arguments if we have any - (when (equal (string-match-p "(" form pos) pos) - (pcase-let* ((`(,new-args . ,new-pos) (read-from-string form pos))) - (setq pos new-pos - args (mapcar #'org-edna--transform-arg new-args)))) - ;; Check for a modifier - (when (string-match "^\\([!]\\)\\(.*\\)" (symbol-name token)) - (setq modifier (intern (match-string 1 (symbol-name token)))) - (setq token (intern (match-string 2 (symbol-name token))))) - ;; Move across any whitespace - (when (string-match "\\s-+" form pos) - (setq pos (match-end 0))) - (list token args modifier pos))) +(defun org-edna-break-modifier (token) + "Break TOKEN into a modifier and base token. + +A modifier is a single character. + +Return (MODIFIER . TOKEN), even if MODIFIER is nil." + (if token + (let (modifier) + (when (string-match "^\\([!]\\)\\(.*\\)" (symbol-name token)) + (setq modifier (intern (match-string 1 (symbol-name token)))) + (setq token (intern (match-string 2 (symbol-name token))))) + (cons modifier token)) + ;; Still return something + '(nil . nil))) (defun org-edna--function-for-key (key) "Determine the Edna function for KEY. KEY should be a symbol, the keyword for which to find the Edna -function." +function. + +If KEY is an invalid Edna keyword, then return nil." (cond - ;; Just return nil if it's not a symbol; `org-edna-process-form' will handle - ;; the rest + ;; Just return nil if it's not a symbol ((or (not key) (not (symbolp key)))) - ((eq key 'consideration) + ((memq key '(consideration consider)) ;; Function is ignored here (cons 'consideration 'identity)) ((string-suffix-p "!" (symbol-name key)) @@ -148,8 +148,170 @@ function." (when (fboundp func-sym) (cons 'finder func-sym)))))) +(defun org-edna-parse-string-form (form &optional start) + "Parse Edna string form FORM starting at position START. + +Return (SEXP-FORM POS) + +SEXP-FORM is the sexp form of FORM starting at START. +POS is the position in FORM where parsing ended." + (setq start (or start 0)) + (pcase-let* ((`(,token . ,pos) (read-from-string form start)) + (args nil)) + (unless token + (org-edna--syntax-error "Invalid Token" form start)) + ;; Check for either end of string or an opening parenthesis + (unless (or (equal pos (length form)) + (equal (string-match-p "\\s-" form pos) pos) + (equal (string-match-p "(" form pos) pos)) + (org-edna--syntax-error "Invalid character in form" form pos)) + ;; Parse arguments if we have any + (when (equal (string-match-p "(" form pos) pos) + (pcase-let* ((`(,new-args . ,new-pos) (read-from-string form pos))) + (setq pos new-pos + args (mapcar #'org-edna--transform-arg new-args)))) + ;; Move across any whitespace + (when (string-match "\\s-+" form pos) + (setq pos (match-end 0))) + (list (cons token args) pos))) + +(defun org-edna--convert-form (string &optional pos) + "Convert string form STRING into a flat sexp form. + +POS is the position in STRING from which to start conversion. + +Returns (FLAT-FORM END-POS) where + +FLAT-FORM is the flat sexp form +END-POS is the position in STRING where parsing ended. + +Example: + +siblings todo!(TODO) => ((siblings) (todo! TODO))" + (let ((pos (or pos 0)) + final-form) + (while (< pos (length string)) + (pcase-let* ((`(,form ,new-pos) (org-edna-parse-string-form string pos))) + (setq final-form (append final-form (list form))) + (setq pos new-pos))) + (cons final-form pos))) + +(defun org-edna--normalize-sexp-form (form action-or-condition &optional from-string) + "Normalize flat sexp form FORM into a full edna sexp form. + +ACTION-OR-CONDITION is either 'action or 'condition, indicating +which of the two types is allowed in FORM. + +FROM-STRING is used internally, and is non-nil if FORM was +originally a string. + +Returns (NORMALIZED-FORM REMAINING-FORM), where REMAINING-FORM is +the remainder of FORM after the current scope was parsed." + (let* ((remaining-form (copy-sequence form)) + (state 'finder) + final-form + need-break) + (while (and remaining-form (not need-break)) + (let ((current-form (pop remaining-form))) + (pcase (car current-form) + ('if + ;; Check the car of each r*-form for the expected + ;; ending. If it doesn't match, throw an error. + (let (cond-form then-form else-form have-else) + (pcase-let* ((`(,temp-form ,r-form) + (org-edna--normalize-sexp-form + remaining-form + ;; Only allow conditions in cond forms + 'condition + from-string))) + ;; Use car-safe to catch r-form = nil + (unless (equal (car-safe r-form) '(then)) + (org-edna--syntax-error + "Malformed if-construct; expected then terminator" + from-string current-form)) + (setq cond-form temp-form + remaining-form (cdr r-form))) + (pcase-let* ((`(,temp-form ,r-form) + (org-edna--normalize-sexp-form remaining-form + action-or-condition + from-string))) + (unless (member (car-safe r-form) '((else) (endif))) + (org-edna--syntax-error + "Malformed if-construct; expected else or endif terminator" + from-string current-form)) + (setq have-else (equal (car r-form) '(else)) + then-form temp-form + remaining-form (cdr r-form))) + (when have-else + (pcase-let* ((`(,temp-form ,r-form) + (org-edna--normalize-sexp-form remaining-form + action-or-condition + from-string))) + (unless (equal (car-safe r-form) '(endif)) + (org-edna--syntax-error "Malformed if-construct; expected endif terminator" + from-string current-form)) + (setq else-form temp-form + remaining-form (cdr r-form)))) + (push `(if ,cond-form ,then-form ,else-form) final-form))) + ((or 'then 'else 'endif) + (setq need-break t) + ;; Push the object back on remaining-form so the if knows where we are + (setq remaining-form (cons current-form remaining-form))) + (_ + ;; Determine the type of the form + ;; If we need to change state, return from this scope + (pcase-let* ((`(,type . ,func) (org-edna--function-for-key (car current-form)))) + (unless (and type func) + (org-edna--syntax-error "Unrecognized Form" + from-string current-form)) + (pcase type + ('finder + (unless (memq state '(finder consideration)) + ;; We changed back to finders, so we need to start a new scope + (setq need-break t))) + ('action + (unless (eq action-or-condition 'action) + (org-edna--syntax-error "Actions aren't allowed in this context" + from-string current-form))) + ('condition + (unless (eq action-or-condition 'condition) + (org-edna--syntax-error "Conditions aren't allowed in this context" + from-string current-form)))) + ;; Update state + (setq state type) + (if need-break ;; changing state + ;; Keep current-form on remaining-form so we have it for the + ;; next scope, since we didn't process it here. + (setq remaining-form (cons current-form remaining-form)) + (push current-form final-form))))))) + (when (and (eq state 'finder) + (eq action-or-condition 'condition)) + ;; Finders have to have something at the end, so we need to add that + ;; something. No default actions, so this must be a blocker. + (push '(!done?) final-form)) + (list (nreverse final-form) remaining-form))) + +(defun org-edna-string-form-to-sexp-form (string-form action-or-condition) + "Parse string form STRING-FORM into an Edna sexp form. + +ACTION-OR-CONDITION is either 'action or 'condition, indicating +which of the two types is allowed in STRING-FORM." + (car + (org-edna--normalize-sexp-form + (car (org-edna--convert-form string-form)) + action-or-condition + string-form))) + (defun org-edna--handle-condition (func mod args targets consideration) - "Handle a condition." + "Handle a condition. + +FUNC is the condition function. +MOD is the modifier to pass to FUNC. +ARGS are any arguments to pass to FUNC. +TARGETS is a list of targets on which to operate. +CONSIDERATION is the consideration symbol, if any." + (when (seq-empty-p targets) + (message "Warning: Condition specified without targets")) ;; Check the condition at each target (when-let* ((blocks (mapcar @@ -160,69 +322,112 @@ function." ;; Apply consideration (org-edna-handle-consideration consideration blocks))) -(defun org-edna-process-form (form action-or-condition) - "Process FORM. - -ACTION-OR-CONDITION is a symbol, either 'action or 'condition, -indicating whether FORM accepts actions or conditions." - (let ((targets) - (blocking-entry) - (consideration 'all) - (state nil) ;; Type of operation - ;; Keep track of the current heading - (last-entry (point-marker)) - (pos 0)) - (while (< pos (length form)) - (pcase-let* ((`(,key ,args ,mod ,new-pos) (org-edna-parse-form form pos)) - (`(,type . ,func) (org-edna--function-for-key key))) - (unless (and key type func) - (org-edna--syntax-error "Unrecognized Form" form pos)) - (pcase type - ('finder - (unless (eq state 'finder) - ;; We just executed some actions, so reset the entries. - (setq targets nil)) - (setq state 'finder) - (let ((markers (apply func args))) - (setq targets (seq-uniq `(,@targets ,@markers))))) - ('action - (unless (eq action-or-condition 'action) - (org-edna--syntax-error "Actions aren't allowed in this context" form pos)) - (unless targets - (message "Warning: Action specified without targets")) - (setq state 'action) - (dolist (target targets) - (org-with-point-at target - (apply func last-entry args)))) - ('condition - (unless (eq action-or-condition 'condition) - (org-edna--syntax-error "Conditions aren't allowed in this context" form pos)) - (unless targets - (message "Warning: Condition specified without targets")) - (setq state 'condition) - (setq blocking-entry - (or blocking-entry ;; We're already blocking - (org-edna--handle-condition func mod args targets consideration)))) - ('consideration - (unless (= (length args) 1) - (org-edna--syntax-error "Consideration requires a single argument" form pos)) - ;; Consideration must be at the start of the targets, so clear out - ;; any old targets. - (setq targets nil - consideration (nth 0 args)))) - (setq pos new-pos))) - ;; We exhausted the input string, but didn't find a condition when we were - ;; expecting one. - (when (and (eq action-or-condition 'condition) ;; Looking for conditions - (eq state 'finder) ;; but haven't found any - (not blocking-entry)) ;; ever - (setq blocking-entry - (org-edna--handle-condition 'org-edna-condition/done? - t nil targets consideration))) - ;; Only blockers care about the return value, and this will be non-nil if - ;; the entry should be blocked. - (setq org-block-entry-blocking blocking-entry) - (not blocking-entry))) +(defun org-edna--add-targets (old-targets new-targets) + "Add targets in NEW-TARGETS to OLD-TARGETS. + +Neither argument is modified." + (seq-uniq (append old-targets new-targets))) + +(defun org-edna--handle-action (action targets last-entry args) + "Process ACTION on TARGETS. + +LAST-ENTRY is the source entry. +ARGS is a list of arguments to pass to ACTION." + (when (seq-empty-p targets) + (message "Warning: Action specified without targets")) + (dolist (target targets) + (org-with-point-at target + (apply action last-entry args)))) + +(defun org-edna--expand-single-sexp-form (single-form + target-var + consideration-var + blocking-var) + "Expand sexp form SINGLE-FORM into a Lisp form. + +TARGET-VAR, BLOCKING-VAR, and CONSIDERATION-VAR are symbols that +correspond to internal variables." + (pcase-let* ((`(,mkey . ,args) single-form) + (`(,mod . ,key) (org-edna-break-modifier mkey)) + (`(,type . ,func) (org-edna--function-for-key key))) + (pcase type + ('finder + `(setq ,target-var (org-edna--add-targets ,target-var (,func ,@args)))) + ('action + `(org-edna--handle-action ',func ,target-var (point-marker) ',args)) + ('condition + `(setq ,blocking-var (or ,blocking-var + (org-edna--handle-condition ',func ',mod ',args + ,target-var + ,consideration-var)))) + ('consideration + `(setq ,consideration-var ,(nth 0 args)))))) + +(defun org-edna--expand-sexp-form (form &optional + use-old-scope + old-target-var + old-consideration-var + old-blocking-var) + "Expand sexp form FORM into a Lisp form. + +USE-OLD-SCOPE, OLD-TARGET-VAR, OLD-CONSIDERATION-VAR, and +OLD-BLOCKING-VAR are used internally." + (when form + ;; We inherit the original targets, consideration, and blocking-entry when + ;; we create a new scope in an if-construct. + (let* ((target-var (if use-old-scope old-target-var (cl-gentemp "targets"))) + (consideration-var (if use-old-scope + old-consideration-var + (cl-gentemp "consideration"))) + (blocking-var (if use-old-scope + old-blocking-var + (cl-gentemp "blocking-entry"))) + ;; These won't be used if use-old-scope is non-nil + (let-binds `((,target-var ,old-target-var) + (,consideration-var ,old-consideration-var) + (,blocking-var ,old-blocking-var))) + (wrapper-form (if use-old-scope + '(progn) + `(let (,@let-binds))))) + (pcase form + (`(if ,cond ,then . ,else) + ;; Don't pass the old variables into the condition form; it should be + ;; evaluated on its own to avoid clobbering the old targets. + `(if (not ,(org-edna--expand-sexp-form cond)) + ,(org-edna--expand-sexp-form + then + '(progn) + old-target-var old-consideration-var old-blocking-var) + ,(when else + (org-edna--expand-sexp-form + ;; else is wrapped in a list, so take the first argument + (car else) + '(progn) + old-target-var old-consideration-var old-blocking-var)))) + ((pred (lambda (arg) (symbolp (car arg)))) + (org-edna--expand-single-sexp-form + form old-target-var old-consideration-var old-blocking-var)) + (_ + ;; List of forms + ;; Only use new variables if we're asked to + `(,@wrapper-form + ,@(mapcar + (lambda (f) (org-edna--expand-sexp-form + f '(progn) target-var consideration-var blocking-var)) + form))))))) + +(defun org-edna-eval-sexp-form (sexp-form) + "Evaluate Edna sexp form SEXP-FORM." + (eval + (org-edna--expand-sexp-form sexp-form))) + +(defun org-edna-process-form (string-form action-or-condition) + "Process STRING-FORM. + +ACTION-OR-CONDITION is either 'action or 'condition, indicating +which of the two types is allowed in STRING-FORM." + (org-edna-eval-sexp-form + (org-edna-string-form-to-sexp-form string-form action-or-condition))) @@ -273,7 +478,8 @@ See `org-edna-run' for CHANGE-PLIST explanation. This shouldn't be run from outside of `org-blocker-hook'." (org-edna-run change-plist (if-let* ((form (org-entry-get pos "BLOCKER" org-edna-use-inheritance))) - (org-edna-process-form form 'condition) + ;; Return nil if there is no blocking entry + (not (setq org-block-entry-blocking (org-edna-process-form form 'condition))) t))) ;;;###autoload @@ -324,7 +530,7 @@ SCOPE defaults to agenda, and SKIP defaults to nil. ;; ID finder (defun org-edna-finder/ids (&rest ids) - "Find a list of headings with given IDs. + "Find a list of headings with given IDS. Edna Syntax: ids(ID1 ID2 ...) @@ -357,7 +563,11 @@ Edna Syntax: self" (point-marker))) (defun org-edna-goto-sibling (&optional previous wrap) - "Move to the next sibling on the same level as the current heading." + "Move to the next sibling on the same level as the current heading. + +If PREVIOUS is non-nil, go to the previous sibling. +f WRAP is non-nil, wrap around when the beginning (or end) is +reached." (let ((next (save-excursion (if previous (org-get-last-sibling) (org-get-next-sibling))))) (cond @@ -384,12 +594,12 @@ Edna Syntax: self" START is a point or marker from which to start collection. -BACKWARDS means go backward through the level instead of forward. +BACKWARD means go backward through the level instead of forward. If WRAP is non-nil, wrap around when the end of the current level is reached. -If INCLUDE-START is non-nil, include the current point." +If INCLUDE-POINT is non-nil, include the current point." (org-with-wide-buffer (let ((markers)) (goto-char start) @@ -402,6 +612,12 @@ If INCLUDE-START is non-nil, include the current point." (nreverse markers)))) (defun org-edna-collect-ancestors (&optional with-self) + "Collect the ancestors of the current subtree. + +If WITH-SELF is non-nil, include the current subtree in the list +of ancestors. + +Return a list of markers for the ancestors." (let ((markers)) (when with-self (push (point-marker) markers)) @@ -411,6 +627,12 @@ If INCLUDE-START is non-nil, include the current point." (nreverse markers))) (defun org-edna-collect-descendants (&optional with-self) + "Collect the descendants of the current subtree. + +If WITH-SELF is non-nil, include the current subtree in the list +of descendants. + +Return a list of markers for the descendants." (let ((targets (org-with-wide-buffer (org-map-entries @@ -422,7 +644,7 @@ If INCLUDE-START is non-nil, include the current point." targets)) (defun org-edna-entry-has-tags-p (&rest tags) - "Returns non-nil if the current entry has any tags in TAGS." + "Return non-nil if the current entry has any tags in TAGS." (when-let* ((entry-tags (org-get-tags-at))) (seq-intersection tags entry-tags))) @@ -491,8 +713,7 @@ All arguments are symbols, unless noted otherwise. - scheduled-up: Scheduled time, farthest first - scheduled-down: Scheduled time, closest first - deadline-up: Deadline time, farthest first -- deadline-down: Deadline time, closest first -" +- deadline-down: Deadline time, closest first" (let (targets sortfun reverse-sort @@ -1021,8 +1242,8 @@ required." (when (= delta 0) (setq delta -7))) (when (> n 1) (setq delta (+ delta (* (1- n) (if (= dir ?-) -7 7))))) (list delta "d" rel)))) - (if (or (not have-landing) - (member what '("M" "h"))) ;; Don't change landing for minutes or hours + (if (or (not have-landing) + (member what '("M" "h"))) ;; Don't change landing for minutes or hours ret ;; Don't worry about landing, just return (pcase-let* ((`(,del ,what _) ret) (mod-index (cdr (assoc what type-strings))) @@ -1076,7 +1297,10 @@ MONTH may be a month string or an integer. Use 0 for the following or previous month. DAY is an optional integer. If not given, it will be 1 (for -forward) or the last day of MONTH (backward)." +forward) or the last day of MONTH (backward). + +Time is computed relative to either THIS-TIME (+/-) or +DEFAULT (++/--)." (require 'parse-time) (let* ((case-fold-search t) (weekdays (mapcar 'car parse-time-weekdays)) @@ -1163,7 +1387,10 @@ forward) or the last day of MONTH (backward)." (list (- abs-days-then abs-days-now) "d" rel))))) (defun org-edna--handle-planning (type last-entry args) - "Handle planning of type TYPE." + "Handle planning of type TYPE. + +LAST-ENTRY is a marker to the source entry. +ARGS is a list of arguments; currently, only the first is used." (let* ((arg (nth 0 args)) (last-ts (org-with-point-at last-entry (org-edna--get-planning-info type))) (this-ts (org-edna--get-planning-info type)) @@ -1508,19 +1735,37 @@ starting from target's position." (defun org-edna-handle-consideration (consideration blocks) "Handle consideration CONSIDERATION. -Edna Syntax: consideration(all) [1] -Edna Syntax: consideration(N) [2] -Edna Syntax: consideration(P) [3] +Edna Syntax: consider(all) [1] +Edna Syntax: consider(N) [2] +Edna Syntax: consider(P) [3] +Edna Syntax: consider(any) [4] Form 1: consider all targets when evaluating conditions. Form 2: consider the condition met if only N of the targets pass. -Form 3: consider the condition met if only P% of the targets pass." - (let ((first-block (seq-find #'identity blocks)) - (total-blocks (seq-length blocks))) +Form 3: consider the condition met if only P% of the targets pass. +Form 4: consider the condition met if any target meets it + +If CONSIDERATION is nil, default to 'all. + +The \"consideration\" keyword is also provided. It functions the +same as \"consider\"." + ;; BLOCKS is a list of blocking entries; if one isn't blocked, its entry will + ;; be nil. + (let ((consideration (or consideration 'all)) + (first-block (seq-find #'identity blocks)) + (total-blocks (seq-length blocks)) + (fulfilled (seq-count #'not blocks))) (pcase consideration ('all ;; All of them must be fulfilled, so find the first one that isn't. first-block) + ('any + ;; Any of them can be fulfilled, so find the first one that is + (if (> fulfilled 0) + ;; Have one fulfilled + nil + ;; None of them are fulfilled + first-block)) ((pred integerp) ;; A fixed number of them must be fulfilled, so check how many aren't. (let* ((fulfilled (seq-count #'not blocks))) @@ -1548,6 +1793,7 @@ Form 3: consider the condition met if only P% of the targets pass." :group 'org-edna) (defun org-edna-in-edit-buffer-p () + "Return non-nil if inside the Edna edit buffer." (string-equal (buffer-name) org-edna-edit-buffer-name)) (defun org-edna-replace-newlines (string) @@ -1560,6 +1806,7 @@ Form 3: consider the condition met if only P% of the targets pass." (marker-position second-marker))) (defun org-edna-edit-blocker-section-text () + "Collect the BLOCKER section text from an edit buffer." (when (org-edna-in-edit-buffer-p) (let ((original-text (org-edna-edit-text-between-markers org-edna-blocker-section-marker @@ -1569,6 +1816,7 @@ Form 3: consider the condition met if only P% of the targets pass." (org-edna-replace-newlines (match-string 1 original-text)))))) (defun org-edna-edit-trigger-section-text () + "Collect the TRIGGER section text from an edit buffer." (when (org-edna-in-edit-buffer-p) (let ((original-text (org-edna-edit-text-between-markers org-edna-trigger-section-marker @@ -1625,6 +1873,7 @@ the source buffer. Finish with `C-c C-c' or abort with `C-c C-k'\n\n") (add-hook 'completion-at-point-functions 'org-edna-completion-at-point nil t))) (defun org-edna-edit-finish () + "Finish an Edna property edit." (interactive) (let ((blocker (org-edna-edit-blocker-section-text)) (trigger (org-edna-edit-trigger-section-text)) @@ -1641,6 +1890,7 @@ the source buffer. Finish with `C-c C-c' or abort with `C-c C-k'\n\n") (kill-buffer org-edna-edit-buffer-name))) (defun org-edna-edit-abort () + "Abort an Edna property edit." (interactive) (let ((pos-marker org-edna-edit-original-marker) (wc org-window-configuration) @@ -1675,6 +1925,9 @@ the source buffer. Finish with `C-c C-c' or abort with `C-c C-k'\n\n") (point-max-marker))) (defun org-edna--collect-keywords (keyword-type &optional suffix) + "Collect known Edna keywords of type KEYWORD-TYPE. + +SUFFIX is an additional suffix to use when matching keywords." (let* ((suffix (or suffix "")) (edna-sym-list) (edna-rx (rx-to-string `(and @@ -1693,12 +1946,15 @@ the source buffer. Finish with `C-c C-c' or abort with `C-c C-k'\n\n") edna-sym-list)) (defun org-edna--collect-finders () + "Return a list of finder keywords." (org-edna--collect-keywords "finder")) (defun org-edna--collect-actions () + "Return a list of action keywords." (org-edna--collect-keywords "action" "!")) (defun org-edna--collect-conditions () + "Return a list of condition keywords." (org-edna--collect-keywords "condition" "?")) (defun org-edna-completions-for-blocker () @@ -1713,6 +1969,10 @@ the source buffer. Finish with `C-c C-c' or abort with `C-c C-k'\n\n") ,@(org-edna--collect-actions))) (defun org-edna-completion-table-function (string pred action) + "Completion table function for Edna keywords. + +See `minibuffer-completion-table' for description of STRING, +PRED, and ACTION." (let ((completions (cond ;; Don't offer completion inside of arguments ((> (syntax-ppss-depth (syntax-ppss)) 0) nil) @@ -1735,6 +1995,7 @@ the source buffer. Finish with `C-c C-c' or abort with `C-c C-k'\n\n") (cycle-sort-function . identity))))))) (defun org-edna-completion-at-point () + "Complete the Edna keyword at point." (when-let* ((bounds (bounds-of-thing-at-point 'symbol))) (list (car bounds) (cdr bounds) 'org-edna-completion-table-function))) @@ -1743,6 +2004,9 @@ the source buffer. Finish with `C-c C-c' or abort with `C-c C-k'\n\n") (declare-function lm-report-bug "lisp-mnt" (topic)) (defun org-edna-submit-bug-report (topic) + "Submit a bug report to the Edna developers. + +TOPIC is the topic for the bug report." (interactive "sTopic: ") (require 'lisp-mnt) (let* ((src-file (locate-library "org-edna.el" t)) diff --git a/packages/org-edna/org-edna.info b/packages/org-edna/org-edna.info index 00bfa6e..1da4b0c 100644 --- a/packages/org-edna/org-edna.info +++ b/packages/org-edna/org-edna.info @@ -20,6 +20,7 @@ Org Edna * Advanced Features:: * Extending Edna:: What else can it do? * Contributing:: I wanna help! +* Changelog:: List of changes by version — The Detailed Node Listing — @@ -77,7 +78,8 @@ Advanced Features * Conditions:: More than just DONE headings * Consideration:: Only some of them -* Setting the properties:: The easy way to set BLOCKER and TRIGGER +* Conditional Forms:: If/Then/Else +* Setting the Properties:: The easy way to set BLOCKER and TRIGGER Conditions @@ -101,6 +103,28 @@ Contributing * Bugs:: * Development:: +* Documentation:: Improving the documentation + +Changelog + +* 1.0beta3: 10beta3. +* 1.0beta2: 10beta2. + +1.0beta3 + +* Conditional Forms: Conditional Forms (1). +* Overhauled Internal Parsing:: +* Fixed consideration keywords:: +* Added 'any consideration:: + + +1.0beta2 + +* Added interactive keyword editor with completion:: +* New uses of schedule! and deadline!:: +* New ``relatives'' finder:: +* New finders:: + @@ -991,7 +1015,8 @@ Advanced Features * Conditions:: More than just DONE headings * Consideration:: Only some of them -* Setting the properties:: The easy way to set BLOCKER and TRIGGER +* Conditional Forms:: If/Then/Else +* Setting the Properties:: The easy way to set BLOCKER and TRIGGER File: org-edna.info, Node: Conditions, Next: Consideration, Up: Advanced Features @@ -1108,32 +1133,39 @@ Any condition can be negated by using ’!’ before the condition. heading tagged “test” does *not* have the property PROP set to “1”. -File: org-edna.info, Node: Consideration, Next: Setting the properties, Prev: Conditions, Up: Advanced Features +File: org-edna.info, Node: Consideration, Next: Conditional Forms, Prev: Conditions, Up: Advanced Features Consideration ============= “Consideration” is a special keyword that’s only valid for blockers. + This says “Allow a task to complete if CONSIDERATION of its targets +pass the given condition”. + This keyword can allow specifying only a portion of tasks to consider: 1. consider(PERCENT) 2. consider(NUMBER) 3. consider(all) (Default) + 4. consider(any) (1) tells the blocker to only consider some portion of the targets. If at least PERCENT of them are in a DONE state, allow the task to be -set to DONE. PERCENT must be a decimal. +set to DONE. PERCENT must be a decimal, and doesn’t need to include a +%-sign. (2) tells the blocker to only consider NUMBER of the targets. (3) tells the blocker to consider all following targets. - A consideration must be specified before the targets to which it + (4) tells the blocker to allow passage if any of the targets pass. + + A consideration must be specified before the conditions to which it applies: - consider(0.5) siblings consider(all) match("find_me") + consider(0.5) siblings match("find_me") consider(all) !done? The above code will allow task completion if at least half the siblings are complete, and all tasks tagged “find_me” are complete. @@ -1146,10 +1178,83 @@ are complete. If no consideration is given, ALL is assumed. + Both “consider” and “consideration” are valid keywords; they both +mean the same thing. + + +File: org-edna.info, Node: Conditional Forms, Next: Setting the Properties, Prev: Consideration, Up: Advanced Features + +Conditional Forms +================= + +Let’s say you’ve got the following checklist: + + * TODO Nightly + DEADLINE: <2017-12-22 Fri 22:00 +1d> + :PROPERTIES: + :ID: 12345 + :BLOCKER: match("nightly") + :TRIGGER: match("nightly") todo!(TODO) + :END: + * TODO Prepare Tomorrow's Lunch :nightly: + * TODO Lock Back Door :nightly: + * TODO Feed Dog :nightly: + + You don’t know in what order you want to perform each task, nor +should it matter. However, you also want the parent heading, “Nightly”, +to be marked as DONE when you’re finished with the last task. + + There are two solutions to this: 1. Have each task attempt to mark +“Nightly” as DONE, which will spam blocking messages after each task. + + The second is to use conditional forms. Conditional forms are +simple; it’s just if/then/else/endif: + + if CONDITION then THEN else ELSE endif + + Here’s how that reads: + + “If CONDITION would not block, execute THEN. Otherwise, execute +ELSE.” + + For our nightly entries, this looks as follows: + + * TODO Prepare Tomorrow's Lunch :nightly: + :PROPERTIES: + :TRIGGER: if match("nightly") then ids(12345) todo!(DONE) endif + :END: + + Thus, we replicate our original blocking condition on all of them, so +it won’t trigger the original until the last one is marked DONE. + + Occasionally, you may find that you’d rather execute a form if the +condition *would* block. There are two options. + + The first is confusing: use ‘consider(any)’. This will tell Edna to +pass so long as one of the targets meets the condition. This is the +opposite of Edna’s standard operation, which only allows passage if all +targets meet the condition. + + * TODO Prepare Tomorrow's Lunch :nightly: + :PROPERTIES: + :TRIGGER: if consider(any) match("nightly") then ids(12345) todo!(DONE) endif + :END: + + The second is a lot easier to understand: just switch the then and +else clauses: + + * TODO Prepare Tomorrow's Lunch :nightly: + :PROPERTIES: + :TRIGGER: if match("nightly") then else ids(12345) todo!(DONE) endif + :END: + + The conditional block tells it to evaluate that section. Thus, you +can conditionally add targets, or conditionally check conditions. + -File: org-edna.info, Node: Setting the properties, Prev: Consideration, Up: Advanced Features +File: org-edna.info, Node: Setting the Properties, Prev: Conditional Forms, Up: Advanced Features -Setting the properties +Setting the Properties ====================== There are two ways to set the BLOCKER and TRIGGER properties: by hand, @@ -1274,7 +1379,7 @@ get our result. blocked. -File: org-edna.info, Node: Contributing, Prev: Extending Edna, Up: Top +File: org-edna.info, Node: Contributing, Next: Changelog, Prev: Extending Edna, Up: Top Contributing ************ @@ -1294,6 +1399,7 @@ We are all happy for any help you may provide. * Bugs:: * Development:: +* Documentation:: Improving the documentation File: org-edna.info, Node: Bugs, Next: Development, Up: Contributing @@ -1310,7 +1416,7 @@ There are two ways to submit bug reports: caused the bug, with as much context as possible. -File: org-edna.info, Node: Development, Prev: Bugs, Up: Contributing +File: org-edna.info, Node: Development, Next: Documentation, Prev: Bugs, Up: Contributing Development =========== @@ -1340,68 +1446,205 @@ We can then merge that into the main development branch. • Avoid additional or altered dependencies if at all possible • Exception: New versions of Org mode are allowed + +File: org-edna.info, Node: Documentation, Prev: Development, Up: Contributing + +Documentation +============= + +Documentation is always helpful to us. Please be sure to do the +following after making any changes: + + 1. Update the info page in the repository with ‘C-c C-e i i’ + 2. If you’re updating the HTML documentation, switch to a theme that + can easily be read on a white background; we recommend the + “adwaita” theme + + +File: org-edna.info, Node: Changelog, Prev: Contributing, Up: Top + +Changelog +********* + +* Menu: + +* 1.0beta3: 10beta3. +* 1.0beta2: 10beta2. + + +File: org-edna.info, Node: 10beta3, Next: 10beta2, Up: Changelog + +1.0beta3 +======== + +HUGE addition here + +* Menu: + +* Conditional Forms: Conditional Forms (1). +* Overhauled Internal Parsing:: +* Fixed consideration keywords:: +* Added 'any consideration:: + + +File: org-edna.info, Node: Conditional Forms (1), Next: Overhauled Internal Parsing, Up: 10beta3 + +Conditional Forms +----------------- + + • See *note Conditional Forms:: for more information + + +File: org-edna.info, Node: Overhauled Internal Parsing, Next: Fixed consideration keywords, Prev: Conditional Forms (1), Up: 10beta3 + +Overhauled Internal Parsing +--------------------------- + + +File: org-edna.info, Node: Fixed consideration keywords, Next: Added 'any consideration, Prev: Overhauled Internal Parsing, Up: 10beta3 + +Fixed consideration keywords +---------------------------- + + • Both consider and consideration are accepted now + + +File: org-edna.info, Node: Added 'any consideration, Prev: Fixed consideration keywords, Up: 10beta3 + +Added ’any consideration +------------------------ + + • Allows passage if just one target is fulfilled + + +File: org-edna.info, Node: 10beta2, Prev: 10beta3, Up: Changelog + +1.0beta2 +======== + +Big release here, with three new features. + +* Menu: + +* Added interactive keyword editor with completion:: +* New uses of schedule! and deadline!:: +* New ``relatives'' finder:: +* New finders:: + + +File: org-edna.info, Node: Added interactive keyword editor with completion, Next: New uses of schedule! and deadline!, Up: 10beta2 + +Added interactive keyword editor with completion +------------------------------------------------ + + • See *note Setting the Properties:: for how to do that + + +File: org-edna.info, Node: New uses of schedule! and deadline!, Next: New ``relatives'' finder, Prev: Added interactive keyword editor with completion, Up: 10beta2 + +New uses of schedule! and deadline! +----------------------------------- + + • New “float” form that mimics diary-float + • New “landing” addition to “+1d” and friends to force planning + changes to land on a certain day or type of day (weekend/weekday) + • See *note Scheduled/Deadline:: for details + + +File: org-edna.info, Node: New ``relatives'' finder, Next: New finders, Prev: New uses of schedule! and deadline!, Up: 10beta2 + +New “relatives” finder +---------------------- + + • Renamed from chain-find with tons of new keywords + • Modified all other relative finders (previous-sibling, first-child, + etc.) to use the same keywords + • See *note relatives:: for details + + +File: org-edna.info, Node: New finders, Prev: New ``relatives'' finder, Up: 10beta2 + +New finders +----------- + + • *note previous-sibling-wrap:: + • *note rest-of-siblings-wrap:: + Tag Table: Node: Top225 -Node: Copying3409 -Node: Introduction4226 -Node: Installation and Setup5174 -Node: Basic Operation5967 -Node: Blockers7818 -Node: Triggers8104 -Node: Syntax8366 -Node: Basic Features9056 -Node: Finders9359 -Node: ancestors11124 -Node: children11718 -Node: descendants12128 -Node: file12650 -Node: first-child13399 -Node: ids13659 -Node: match14320 -Node: next-sibling14958 -Node: next-sibling-wrap15215 -Node: olp15529 -Node: org-file15941 -Node: parent16586 -Node: previous-sibling16784 -Node: previous-sibling-wrap17045 -Node: relatives17324 -Node: rest-of-siblings20945 -Node: rest-of-siblings-wrap21230 -Node: self21579 -Node: siblings21740 -Node: siblings-wrap21977 -Node: Actions22281 -Node: Scheduled/Deadline23023 -Node: TODO State26598 -Node: Archive26966 -Node: Chain Property27286 -Node: Clocking27569 -Node: Property27981 -Node: Priority28303 -Node: Tag28872 -Node: Effort29089 -Node: Advanced Features29478 -Node: Conditions29816 -Node: done30431 -Node: headings30595 -Node: todo-state30971 -Node: variable-set31227 -Node: has-property31656 -Node: re-search31925 -Node: Negating Conditions32285 -Node: Consideration32672 -Node: Setting the properties33904 -Node: Extending Edna34984 -Node: Naming Conventions35474 -Node: Finders (1)35937 -Node: Actions (1)36303 -Node: Conditions (1)36768 -Node: Contributing37658 -Node: Bugs38130 -Node: Development38482 +Node: Copying3930 +Node: Introduction4747 +Node: Installation and Setup5695 +Node: Basic Operation6488 +Node: Blockers8339 +Node: Triggers8625 +Node: Syntax8887 +Node: Basic Features9577 +Node: Finders9880 +Node: ancestors11645 +Node: children12239 +Node: descendants12649 +Node: file13171 +Node: first-child13920 +Node: ids14180 +Node: match14841 +Node: next-sibling15479 +Node: next-sibling-wrap15736 +Node: olp16050 +Node: org-file16462 +Node: parent17107 +Node: previous-sibling17305 +Node: previous-sibling-wrap17566 +Node: relatives17845 +Node: rest-of-siblings21466 +Node: rest-of-siblings-wrap21751 +Node: self22100 +Node: siblings22261 +Node: siblings-wrap22498 +Node: Actions22802 +Node: Scheduled/Deadline23544 +Node: TODO State27119 +Node: Archive27487 +Node: Chain Property27807 +Node: Clocking28090 +Node: Property28502 +Node: Priority28824 +Node: Tag29393 +Node: Effort29610 +Node: Advanced Features29999 +Node: Conditions30383 +Node: done30998 +Node: headings31162 +Node: todo-state31538 +Node: variable-set31794 +Node: has-property32223 +Node: re-search32492 +Node: Negating Conditions32852 +Node: Consideration33239 +Node: Conditional Forms34808 +Node: Setting the Properties37464 +Node: Extending Edna38548 +Node: Naming Conventions39038 +Node: Finders (1)39501 +Node: Actions (1)39867 +Node: Conditions (1)40332 +Node: Contributing41222 +Node: Bugs41773 +Node: Development42125 +Node: Documentation43278 +Node: Changelog43723 +Node: 10beta343868 +Node: Conditional Forms (1)44126 +Node: Overhauled Internal Parsing44325 +Node: Fixed consideration keywords44522 +Node: Added 'any consideration44781 +Node: 10beta244996 +Node: Added interactive keyword editor with completion45278 +Node: New uses of schedule! and deadline!45577 +Node: New ``relatives'' finder46072 +Node: New finders46468 End Tag Table diff --git a/packages/org-edna/org-edna.org b/packages/org-edna/org-edna.org index f42e445..c23f80a 100644 --- a/packages/org-edna/org-edna.org +++ b/packages/org-edna/org-edna.org @@ -7,7 +7,7 @@ #+STARTUP: indent #+TODO: FIXME | FIXED #+OPTIONS: toc:2 num:nil timestamp:nil \n:nil |:t ':t email:t -#+OPTIONS: *:t <:t d:nil todo:nil pri:nil tags:not-in-toc +#+OPTIONS: *:t <:t d:nil todo:nil pri:nil tags:not-in-toc -:nil #+TEXINFO_DIR_CATEGORY: Emacs #+TEXINFO_DIR_TITLE: Org Edna: (org-edna) @@ -928,24 +928,30 @@ tagged "test" does *not* have the property PROP set to "1". "Consideration" is a special keyword that's only valid for blockers. +This says "Allow a task to complete if CONSIDERATION of its targets pass the +given condition". + This keyword can allow specifying only a portion of tasks to consider: 1. consider(PERCENT) 2. consider(NUMBER) 3. consider(all) (Default) +4. consider(any) (1) tells the blocker to only consider some portion of the targets. If at least PERCENT of them are in a DONE state, allow the task to be set to DONE. PERCENT -must be a decimal. +must be a decimal, and doesn't need to include a %-sign. (2) tells the blocker to only consider NUMBER of the targets. (3) tells the blocker to consider all following targets. -A consideration must be specified before the targets to which it applies: +(4) tells the blocker to allow passage if any of the targets pass. + +A consideration must be specified before the conditions to which it applies: #+BEGIN_EXAMPLE -consider(0.5) siblings consider(all) match("find_me") +consider(0.5) siblings match("find_me") consider(all) !done? #+END_EXAMPLE The above code will allow task completion if at least half the siblings are @@ -960,9 +966,90 @@ are complete, and at least two of ID3, ID4, ID5, and ID6 are complete. If no consideration is given, ALL is assumed. -** Setting the properties +Both "consider" and "consideration" are valid keywords; they both mean the same +thing. + +** Conditional Forms +:PROPERTIES: +:CUSTOM_ID: conditional_forms +:DESCRIPTION: If/Then/Else +:END: + +Let's say you've got the following checklist: + +#+begin_src org +,* TODO Nightly + DEADLINE: <2017-12-22 Fri 22:00 +1d> + :PROPERTIES: + :ID: 12345 + :BLOCKER: match("nightly") + :TRIGGER: match("nightly") todo!(TODO) + :END: +,* TODO Prepare Tomorrow's Lunch :nightly: +,* TODO Lock Back Door :nightly: +,* TODO Feed Dog :nightly: +#+end_src + +You don't know in what order you want to perform each task, nor should it +matter. However, you also want the parent heading, "Nightly", to be marked as +DONE when you're finished with the last task. + +There are two solutions to this: 1. Have each task attempt to mark "Nightly" as +DONE, which will spam blocking messages after each task. + +The second is to use conditional forms. Conditional forms are simple; it's just +if/then/else/endif: + +#+begin_quote +if CONDITION then THEN else ELSE endif +#+end_quote + +Here's how that reads: + +"If CONDITION would not block, execute THEN. Otherwise, execute ELSE." + +For our nightly entries, this looks as follows: + +#+begin_src org +,* TODO Prepare Tomorrow's Lunch :nightly: + :PROPERTIES: + :TRIGGER: if match("nightly") then ids(12345) todo!(DONE) endif + :END: +#+end_src + +Thus, we replicate our original blocking condition on all of them, so it won't +trigger the original until the last one is marked DONE. + +Occasionally, you may find that you'd rather execute a form if the condition +*would* block. There are two options. + +The first is confusing: use ~consider(any)~. This will tell Edna to pass so +long as one of the targets meets the condition. This is the opposite of Edna's +standard operation, which only allows passage if all targets meet the condition. + +#+begin_src org +,* TODO Prepare Tomorrow's Lunch :nightly: + :PROPERTIES: + :TRIGGER: if consider(any) match("nightly") then ids(12345) todo!(DONE) endif + :END: +#+end_src + +The second is a lot easier to understand: just switch the then and else clauses: + +#+begin_src org +,* TODO Prepare Tomorrow's Lunch :nightly: + :PROPERTIES: + :TRIGGER: if match("nightly") then else ids(12345) todo!(DONE) endif + :END: +#+end_src + +The conditional block tells it to evaluate that section. Thus, you can +conditionally add targets, or conditionally check conditions. + +** Setting the Properties :PROPERTIES: :DESCRIPTION: The easy way to set BLOCKER and TRIGGER +:CUSTOM_ID: setting_keywords :END: There are two ways to set the BLOCKER and TRIGGER properties: by hand, or the @@ -990,7 +1077,6 @@ of any valid keyword within the BLOCKER or TRIGGER sections using When finished, type ~C-c C-c~ to apply the changes, or ~C-c C-k~ to throw out your changes. - * Extending Edna :PROPERTIES: :DESCRIPTION: What else can it do? @@ -1135,3 +1221,48 @@ There are a few rules to follow: - Run 'make check' to verify that your mods don't break anything - Avoid additional or altered dependencies if at all possible - Exception: New versions of Org mode are allowed + +** Documentation +:PROPERTIES: +:CUSTOM_ID: docs +:DESCRIPTION: Improving the documentation +:END: + +Documentation is always helpful to us. Please be sure to do the following after +making any changes: + +1. Update the info page in the repository with ~C-c C-e i i~ +2. If you're updating the HTML documentation, switch to a theme that can easily + be read on a white background; we recommend the "adwaita" theme +* Changelog +:PROPERTIES: +:DESCRIPTION: List of changes by version +:END: +** 1.0beta3 +HUGE addition here +*** Conditional Forms +- See [[#conditional_forms][Conditional Forms]] for more information +*** Overhauled Internal Parsing +*** Fixed consideration keywords +- Both consider and consideration are accepted now +*** Added 'any consideration +- Allows passage if just one target is fulfilled +** 1.0beta2 +Big release here, with three new features. + +*** Added interactive keyword editor with completion +- See [[#setting_keywords][Setting the Properties]] for how to do that + +*** New uses of schedule! and deadline! +- New "float" form that mimics diary-float +- New "landing" addition to "+1d" and friends to force planning changes to land on a certain day or type of day (weekend/weekday) +- See [[#planning][Scheduled/Deadline]] for details + +*** New "relatives" finder +- Renamed from chain-find with tons of new keywords +- Modified all other relative finders (previous-sibling, first-child, etc.) to use the same keywords +- See [[#relatives][relatives]] for details + +*** New finders +- [[#previous-sibling-wrap][previous-sibling-wrap]] +- [[#rest-of-siblings-wrap][rest-of-siblings-wrap]]