# Bazaar merge directive format 2 (Bazaar 0.90) # revision_id: s.vlasov@uvt.nl-20220527095515-l3211a15pvv5lhql # target_branch: https://bzr.savannah.gnu.org/r/org-edna-el/ # testament_sha1: aaaf60d29db29eb00ce84fb17f1169bb886bbd9a # timestamp: 2022-05-27 11:55:42 +0200 # base_revision_id: dunni@gnu.org-20200902134345-3r1qy8aq46wnjet7 # # Begin patch === modified file 'org-edna.el' --- org-edna.el 2020-09-02 13:41:12 +0000 +++ org-edna.el 2022-05-27 09:55:15 +0000 @@ -85,8 +85,17 @@ \"long\" (without quotes). 4. Fallback to this variable." - :type '(choice (const :tag "Short Format" short) - (const :tag "Long Format" long))) + :group 'org-edna + :type '(choice (const :tag "Short Format" 'short) + (const :tag "Long Format" 'long))) + +(defcustom org-edna-default-clause-sexp '(done:) + "Default clause that activates BLOCKER or TRIGER." + :type 'list) + +(defcustom org-edna-default-condition-sexp '(!done?) + "Default condition of BLOCKER." + :type 'list) (defcustom org-edna-from-todo-states 'todo "Category of TODO states that allow Edna to run. @@ -98,9 +107,45 @@ If `not-done', Edna will run when changing TODO state from any entry that's not in `org-done-keywords'. This includes TODO -state being empty." +state being empty. + +If `nil' Edna will run when changing TODO state from anything +at all including same state and empty state." :type '(choice (const :tag "TODO Keywords" todo) - (const :tag "Not DONE Keywords" not-done))) + (const :tag "Not DONE Keywords" not-done) + (const :tag "Not DONE Keywords" nil))) + + +;;; Accessing `org-todo-keywords' +(defun org-nth-todo-keywords (n &optional type) + "Gets the Nth todo keywords specified in `org-todo-keywords'. N starts from 1. If N is negative get from the end. + +If TYPE is `'todo' then only condider not done keywords. + +If TYPE is `'done' then only condider done keywords." + (unless (numberp n) (error "in org-nth-todo-keywords n should be numeric")) + (thread-last org-todo-keywords + ;; only consider `sequence' type of keywords (type is obsolite anyway) + (seq-filter (lambda (keywords) (equal (car keywords) 'sequence))) + ;; remove `sequence' symbol + (mapcar (lambda (keywords) (cdr keywords))) + ;; filter keywords by TYPE + (mapcar (lambda (keywords) + (pcase type + ('todo (seq-take-while (lambda (keyword) (not (string= keyword "|"))) keywords)) + ('done (seq-drop-while (lambda (keyword) (not (string= keyword "|"))) keywords)) + (_ keywords)))) + ;; remove todo/done separator + (mapcar (lambda (keywords) (seq-remove (lambda (keyword) (string= keyword "|")) keywords))) + ;; get Nth keyword + (mapcar (lambda (keywords) + (if (< n 0) + (nth (1- (- n)) (nreverse keywords)) + (nth (1- n) keywords)))) + ;; remove nil keywords + (seq-filter 'identity) + ;; clean keywords cookies + (mapcar (lambda (keyword) (replace-regexp-in-string "(.+)" "" keyword))))) ;;; Form Parsing @@ -191,6 +236,11 @@ (let ((func-sym (intern (format "org-edna-action/%s" key)))) (when (fboundp func-sym) (cons 'action func-sym)))) + ((string-suffix-p ":" (symbol-name key)) + ;; Clause + (let ((func-sym (intern (format "org-edna-clause/%s" key)))) + (when (fboundp func-sym) + (cons 'clause func-sym)))) ((string-suffix-p "?" (symbol-name key)) ;; Condition (let ((func-sym (intern (format "org-edna-condition/%s" key)))) @@ -351,7 +401,7 @@ (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)) + (push org-edna-default-condition-sexp final-form)) (list (nreverse final-form) remaining-form))) (defun org-edna--normalize-forms (form-list action-or-condition end-forms &optional from-string) @@ -384,15 +434,19 @@ originally a string." (car-safe (org-edna--normalize-forms form-list action-or-condition nil from-string))) -(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." - (org-edna--normalize-all-forms - (car (org-edna--convert-form string-form)) - action-or-condition - string-form)) + +;; not needed because normalization (i.e. org-edna--normalize-all-forms) +;; is done in org-edna--expand-clause-sexp-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." +;; (org-edna--normalize-all-forms +;; (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. @@ -518,17 +572,110 @@ (defun org-edna-eval-sexp-form (sexp-form) "Evaluate Edna sexp form SEXP-FORM." - (eval - (org-edna--expand-sexp-form sexp-form) - t)) - -(defun org-edna-process-form (string-form action-or-condition) + (eval sexp-form t)) + +;; new functions for clause feature + +(defun org-edna--sexp-type-clause-p (sexp) + "Check if sexp is type of clause" + (equal (car (org-edna--function-for-key (caar sexp))) 'clause)) + +;; TEST +;; (org-edna--sexp-type-clause-p '((doin=) . -1)) + +(defun org-edna--sexp-type-not-clause-p (sexp) + "Negation of `org-edna--sexp-type-clause-p'" + (not (org-edna--sexp-type-clause-p sexp))) + +(defun org-edna--clause-partition-sexp-list-form (sexp) + "Partitions the sexp list form by clauses." + (let (clauses) + (while sexp + (let ((clause-key (pop sexp)) + (clause-body + (seq-take-while 'org-edna--sexp-type-not-clause-p sexp))) + (push (cons clause-key clause-body) clauses) + (setq sexp + (seq-drop-while 'org-edna--sexp-type-not-clause-p sexp)))) + (reverse clauses))) + +(defun org-edna--check-initial-clause-form (form-list) + "Check if the first sexp in FORM-LIST is type 'clause and if it is not push `org-edna-default-clause-sexp' to the front." + (if (org-edna--sexp-type-clause-p (car form-list)) + form-list + (push `(,org-edna-default-clause-sexp . -1) form-list))) + +;; TEST +;; (org-edna--check-initial-clause-form '(((lala)) ((if)) ((done? "F")))) +;; (org-edna--check-initial-clause-form '(((state= "DONE")) ((if)) ((done? "F")))) + +;; a la main engine for clause feature implementation +(defun org-edna--expand-clause-sexp-form (sexp-list action-or-condition change-plist) + "DOCSTRING" + (let* ((clause-sexp (car (pop sexp-list))) + (clause (cdr (org-edna--function-for-key (pop clause-sexp)))) + (args clause-sexp)) + `((org-edna--handle-clause ',change-plist ',clause ',args) + ,(org-edna--expand-sexp-form + (org-edna--normalize-all-forms sexp-list action-or-condition))))) + +;; TEST +;; [[id:org:n09d4ge1vfi0][check how basic functions work with new keyword]] + +;; (org-edna--expand-clause-sexp-form + ;; '(((doin=) . -1) ((ids org:n09d4ge1vfi0) . 0) ((!done?) . 10)) + ;; 'condition + ;; '(:type 'todo-state-change :from "TODO" :to "DOIN")) + +(defun org-edna--handle-clause (change-plist clause args) + "Process CLAUSE on CHANGE-PLIST. + +ARGS is a list of arguments to pass to CLAUSE." + ;; TODO: add negation of clauses, e.g., !done= with org-edna-break-modifier + (apply clause change-plist args)) + + +;; old version of org-edna-process-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))) + +;; new version of org-edna-process-form that accepts with change-plist +;; I do not need now org-edna-string-form-to-sexp-form because +;; normalization (org-edna--normalize-all-forms) is done in +;; org-edna--expand-clause-sexp-form now +(defun org-edna-process-form (string-form action-or-condition change-plist) "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))) +which of the two types is allowed in STRING-FORM. + +CHANGE-PLIST is what `org-edna-trigger-function' and `org-edna-blocker-function' receives when called by `org-trigger-hook' or `org-blocker-hook' respectively (see `org-trigger-hook' for details)." + (thread-last string-form + ;; make sexp list + (org-edna--convert-form) + (car) + ;; add default clause to front if there is none + (org-edna--check-initial-clause-form) + ;; partition the list by clause + (org-edna--clause-partition-sexp-list-form) + ;; make conditionional statemets from list of clauses + (mapcar + (lambda (clause) + (org-edna--expand-clause-sexp-form clause action-or-condition change-plist))) + ;; prepend cond command to condition statemens and eval + (cons 'cond) + (org-edna-eval-sexp-form))) + +;; TEST +;; (org-edna-process-form + ;; "doin= ids(org:n09d4ge1vfi0) !done? done= ids(org:n09d4ge1vfi0) done?" + ;; 'condition + ;; (list :type 'todo-state-change :from "DOIN" :to "DONE")) ;;; Cache @@ -657,28 +804,6 @@ ;;; Interactive Functions -(defun org-enda--should-run-in-from-state-p (from) - (pcase org-edna-from-todo-states - ('todo - (member from (cons 'todo org-not-done-keywords))) - ('not-done - (not (member from (cons 'done org-done-keywords)))))) - -(defun org-edna--should-run-p (change-plist) - "Check if Edna should run. - -The state information is held in CHANGE-PLIST. If the TODO state -is changing from a TODO state to a DONE state, run BODY." - (let* ((type (plist-get change-plist :type)) - (from (plist-get change-plist :from)) - (to (plist-get change-plist :to))) - (and - ;; We are only handling todo-state-change - (eq type 'todo-state-change) - ;; And only from a TODO state to a DONE state - (org-enda--should-run-in-from-state-p from) - (member to (cons 'done org-done-keywords))))) - (defmacro org-edna-run (&rest body) "Run a TODO state change." (declare (indent 0)) @@ -695,31 +820,29 @@ (defun org-edna-trigger-function (change-plist) "Trigger function work-horse. -See `org-edna-run' for CHANGE-PLIST explanation. +See `org-trigger-hook' for CHANGE-PLIST explanation. This shouldn't be run from outside of `org-trigger-hook'." - (when (org-edna--should-run-p change-plist) - (org-edna-run - (when-let* ((form (org-entry-get (plist-get change-plist :position) - "TRIGGER" org-edna-use-inheritance))) - (org-edna-process-form form 'action))))) + (org-edna-run + (when-let* ((form (org-entry-get (plist-get change-plist :position) + "TRIGGER" org-edna-use-inheritance))) + (org-edna-process-form form 'action change-plist)))) (defun org-edna-blocker-function (change-plist) "Blocker function work-horse. -See `org-edna-run' for CHANGE-PLIST explanation. +See `org-blocker-hook' for CHANGE-PLIST explanation. This shouldn't be run from outside of `org-blocker-hook'." - (if (org-edna--should-run-p change-plist) - (org-edna-run - (if-let* ((form (org-entry-get (plist-get change-plist :position) - "BLOCKER" org-edna-use-inheritance))) - ;; Return nil if there is no blocking entry - (not (setq org-block-entry-blocking (org-edna-process-form form 'condition))) - t)) - ;; Return t for the blocker to let the calling function know that there - ;; is no block here. - t)) + (org-edna-run + (if-let* ((form (org-entry-get (plist-get change-plist :position) + "BLOCKER" org-edna-use-inheritance))) + ;; Return nil if there is no blocking entry + (not (setq org-block-entry-blocking + (org-edna-process-form form 'condition change-plist))) + ;; Return t for the blocker to let the calling function know that there + ;; is no block here. + t))) ;;;###autoload (defun org-edna--load () @@ -737,8 +860,80 @@ Remove Edna's workers from `org-trigger-hook' and `org-blocker-hook'." - (remove-hook 'org-trigger-hook #'org-edna-trigger-function) - (remove-hook 'org-blocker-hook #'org-edna-blocker-function)) + (interactive) + (remove-hook 'org-trigger-hook 'org-edna-trigger-function) + (remove-hook 'org-blocker-hook 'org-edna-blocker-function)) + + +;;; Clauses + +;; State Change Clause +(defun org-edna-clause/state-change: (change-plist &rest args) + "Checks if the todo state is changed from one state to another. + +ARGS is list of two elements (CHANGE-FROM CHANGE-TO). + +CHANGE-FROM and CHANGE-TO can be either strings or lists of strings independently. If CHANGE-FROM is nil then it falls back to the definition of `org-edna-from-todo-states'. If CHANGE-TO is nil then any new state (including empty and the same state) would be a valid clause and will trigger EDNA blockers and triggers." + (let ((type (plist-get change-plist :type)) + (from (plist-get change-plist :from)) + (to (plist-get change-plist :to)) + (change-from (pop args)) + (change-to (car args))) + (and + ;; We are only handling todo-state-change + (eq type 'todo-state-change) + ;; Check if state is changed from CHANGE-FROM state(s) + (if change-from + (if (listp change-from) + (member from change-from) + (equal from change-from)) + ;; ensure backcompatibility with `org-edna-from-todo-states' + (pcase org-edna-from-todo-states + ('todo (member from org-not-done-keywords)) + ('not-done (not (member from org-done-keywords))) + ('nil t))) + ;; Check if state is changed to CHANGE-TO state(s) + (or (not change-to) + (if (listp change-to) + (member to change-to) + (equal to change-to))) + t))) + +;; ;; TEST +;; (let ((org-not-done-keywords '("TODO" "DOIN")) +;; (org-done-keywords '("DONE" "GONE"))) +;; (org-edna-clause/state-change: +;; '(:type todo-state-change :from "TODO" :to "DONE") +;; nil "DONE")) + +;; ;; TEST +;; (let ((org-not-done-keywords '("TODO" "DOIN")) +;; (org-done-keywords '("DONE" "GONE")) +;; (org-edna-from-todo-states 'not-done)) +;; (org-edna-clause/state-change: +;; '(:type todo-state-change :from "" :to "DONE") +;; nil "DONE")) + +;; State Clause +(defun org-edna-clause/state: (change-plist &rest args) + "Checks if the state is changed to ARGS state(s) from any other state. + +ARGS is either a string or lists of strings." + (org-edna-clause/state-change: change-plist nil args)) + +;; State Change from todo to done +(defun org-edna-clause/done: (change-plist) + "Checks if states change from `org-edna-from-todo-states' (see for the definition) to `org-done-keywords'." + (org-edna-clause/state-change: change-plist nil org-done-keywords)) + +;; TEST +;; (let ((org-not-done-keywords '("TODO" "DOIN")) +;; (org-done-keywords '("DONE" "GONE"))) +;; (org-edna-clause/done: '(:type todo-state-change :from "TODO" :to "DONE"))) + +(defun org-edna-clause/doin: (change-plist) + "Checks if states change from `org-edna-from-todo-states' (see for the definition) to the last not done keywords (see `org-nth-todo-keywords' for (org-nth-todo-keywords -1 'todo))." + (org-edna-clause/state-change: change-plist nil (org-nth-todo-keywords -1 'todo))) (define-obsolete-function-alias 'org-edna-unload #'org-edna-mode "1.1.1") @@ -794,17 +989,25 @@ Edna Syntax: ids(ID1 ID2 ...) -Each ID is a UUID as understood by `org-id-find'. Alternatively, -ID may also be id:UUID, where UUID is a UUID as understood by +Each ID is a UUID as understood by `org-id-find'. Alternatively, +ID may also be id:UUID or [[id:UUID][TITLE]] (should be quoted), where UUID is a UUID as understood by `org-id-find'. -Note that in the edna syntax, the IDs don't need to be quoted." +Note that in the edna syntax, the IDs don't need to be quoted unless it is an in form of org link [[id:UUID][TITLE]]." (mapcar (lambda (id) - (if (string-prefix-p "id:" id) - (org-id-find (string-remove-prefix "id:" id) 'marker) - (org-id-find id 'marker))) - ids)) + (when (symbolp id) + ;; allow not to qoute symbols + (setq id (symbol-name id))) + (when (stringp id) + (when (string-match org-link-bracket-re id) + ;; allow reqular org links [[id:UUID][TITLE]] + (setq id (match-string 1 id))) + (when (string-prefix-p "id:" id) + ;; allow to use id:UUID + (setq id (string-remove-prefix "id:" id)))) + (org-id-find id 'marker)) +ids)) (defun org-edna-finder/self () "Finder for the current heading. @@ -2106,6 +2309,17 @@ (member (org-entry-get nil "TODO") org-done-keywords)))) (org-get-heading))) + +(defun org-edna-condition/doin? (neg) + "Condition to check if all target headings are in the DOIN state (i.e., not the first todo state)." + (when-let* ((condition + (if neg + (member (org-entry-get nil "TODO") + (org-first-todo-keywords)) + (member (org-entry-get nil "TODO") + (append (org-nth-todo-keywords -1 'todo) org-done-keywords 'done))))) + (org-get-heading))) + (defun org-edna-condition/todo-state? (neg state) "Condition to check if all target headings have the TODO state STATE. @@ -2439,15 +2653,21 @@ "Return a list of condition keywords." (org-edna--collect-keywords "condition" "?")) +(defun org-edna--collect-clauses () + "Return a list of clause keywords." + (org-edna--collect-keywords "clause" ":")) + (defun org-edna-completions-for-blocker () "Return a list of all allowed Edna keywords for a blocker." - `(,@(org-edna--collect-finders) + `(,@(org-edna--collect-clauses) + ,@(org-edna--collect-finders) ,@(org-edna--collect-conditions) "consideration" "consider")) (defun org-edna-completions-for-trigger () "Return a list of all allowed Edna keywords for a trigger." - `(,@(org-edna--collect-finders) + `(,@(org-edna--collect-clauses) + ,@(org-edna--collect-finders) ,@(org-edna--collect-actions))) (defun org-edna-completion-table-function (string pred action) @@ -2505,6 +2725,38 @@ (with-help-window (help-buffer) (princ doc)))) + + +;;; Interactively add org ids to blocker property (concept) + +(defun org-edna-add-id-to-first-blocker-ids (id-string) + "Add ids to EDNA's ids() finder in :BLOCKER: property for entry at point." + (interactive "sType in org ids to add to blocker: ") + (let* ((blocker-str + (or (org-entry-get (point) "BLOCKER") "ids()")) + (blocker-ids + (thread-last blocker-str + (org-edna--convert-form) + (car) + (seq-find (lambda (s) (equal (caar s) 'ids))) + (cdar)))) + (setq blocker-str + (with-temp-buffer + (insert blocker-str) + (goto-char (point-min)) + (search-forward "ids(") + (dolist (id blocker-ids) (search-forward id)) + (search-forward ")") + (backward-char) + (unless (looking-back "(\\| ") (insert " ")) + (insert "\"" id-string "\"") + (buffer-string))) + (org-entry-put (point) "BLOCKER" blocker-str))) + + + + + ;;; Bug Reports === modified file 'org-edna.org' --- org-edna.org 2020-09-02 13:43:45 +0000 +++ org-edna.org 2022-05-27 09:55:15 +0000 @@ -148,6 +148,8 @@ ,* TODO Commit Code to Repository #+END_SRC + + ** Blockers :PROPERTIES: :CUSTOM_ID: blockers @@ -1007,7 +1009,7 @@ :DESCRIPTION: More than just DONE headings :END: -Edna gives you he option to specify *blocking conditions*. Each condition is checked +Edna gives you the option to specify *blocking conditions*. Each condition is checked for each of the specified targets; if one of the conditions returns true for that target, then the source heading is blocked. @@ -1421,7 +1423,17 @@ This will only increment the TEST property if the new TODO state is *not* "COMPLETE". Currently, there is no way to take different actions depending on -the *old* TODO state. +the *old* TODO state. However, see experimental [[#clauses][clauses]] feature + +** Clauses (experimental) +:PROPERTIES: +:CUSTOM_ID: clauses +:DESCRIPTION: Case for triggering EDNA actions after completing a task +:END: + +A clause specifies what user does on an org-mode entry so that it can be blocked by a condition specified in a [[#blockers][:BLOCKER:]] property or/and trigger the actions specified in [[#triggers][:TRIGGER:]] property. For example, blocking condition can be checked and actions can be triggered when user attempts to mark heading as DONE (i.e. with one of org-mode-done-keywords) which is the default clause. Marking heading with the next todo keyword or with the second todo keyword or with the last todo keyword are all slightly different clauses for EDNA. + +This allows EDNA to be triggered by different todo state change events (i.e., not only marking heading to DONE). For example, if one have a workflow where todo states sequence is specified as TODO -> DOING -> DONE (see ~org-todo-keywords~) and would like for example to block the starting of work (i.e. TODO -> DOING) with some condition. ** Setting the Properties :PROPERTIES: @@ -1717,6 +1729,18 @@ :PROPERTIES: :DESCRIPTION: List of changes by version :END: +** 1.2.0beta +- Allow full org ~id:~ syntax in ~ids~ finder to allow adding a description part of the link (i.e. ~[[id:link][description]]~) + - Added interactive function ~org-edna-add-id-to-first-blocker-ids~ to insert org ids to EDNA's ids() finder in :BLOCKER: property for the entry at point +- Added experimental [[#clauses][clauses]] feature that allows to specify run conditions for EDNA. To differentiate the clauses the clause keywords end with ':' suffix. Clause keywords go before finders and conditions. Multiple clause are possible (e.g., :BLOCKER: clause:(args) finders conditions clause:(args) finders conditions). Currently the following clauses are implemented: + - ~org-edna-clause/state-change:~ (e.g. :BLOCKER: state-change:("TODO" "DONE") finders conditions) + - ~org-edna-clause/state:~ (e.g. :BLOCKER: state-change:("DOING") finders conditions) + - ~org-edna-clause/done:~ + - ~org-edna-clause/doin:~ +- Added settings ~org-edna-default-condition-sexp~ and ~org-edna-default-clause-sexp~ to allow changing the defaults blocking conditions and run conditions respectively +- Added ~org-nth-todo-keywords~ utility for accessing specific ~org-todo-keywords~ (e.g., it can convinienly get last not done keywords from keywords sequences) +- Added new condition ~org-edna-condition/doin?~ for checking if the entry is in so-called 'DOING' state (i.e., last not done keyword in sequence accessed with ~org-nth-todo-keywords~ utility) + ** 1.1.2 - Allow ~id:~ syntax in ~ids~ finder - Added setting ~org-edna-from-todo-states~ # Begin bundle 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