Also obey to repeaters in inactive time stamps

* lisp/org.el (org-repeat-re): Accept inactive time stamps.
(org-auto-repeat-maybe): Small refactoring.  Find additional repeaters
also in inactive time stamps.
* testing/lisp/test-org.el (test-org/auto-repeat-maybe): Add test.

Reported-by: Leo Gaspard <orgmode@leo.gaspard.io>
<http://lists.gnu.org/r/emacs-orgmode/2018-11/msg00078.html>
This commit is contained in:
Nicolas Goaziou 2018-11-10 18:46:20 +01:00
parent 88ef9f26de
commit af81211fdc
2 changed files with 112 additions and 109 deletions

View File

@ -674,7 +674,8 @@ on a string that terminates immediately after the date.")
The time stamps may be either active or inactive.") The time stamps may be either active or inactive.")
(defconst org-repeat-re (defconst org-repeat-re
"<[0-9]\\{4\\}-[0-9][0-9]-[0-9][0-9] [^>\n]*?\\([.+]?\\+[0-9]+[hdwmy]\\(/[0-9]+[hdwmy]\\)?\\)" "[[<][0-9]\\{4\\}-[0-9][0-9]-[0-9][0-9] [^]>\n]*?\
\\([.+]?\\+[0-9]+[hdwmy]\\(/[0-9]+[hdwmy]\\)?\\)"
"Regular expression for specifying repeated events. "Regular expression for specifying repeated events.
After a match, group 1 contains the repeat expression.") After a match, group 1 contains the repeat expression.")
@ -12730,117 +12731,110 @@ This function is run automatically after each state change to a DONE state."
(org-log-done nil) (org-log-done nil)
(org-todo-log-states nil) (org-todo-log-states nil)
(end (copy-marker (org-entry-end-position)))) (end (copy-marker (org-entry-end-position))))
(unwind-protect (when (and repeat (not (= 0 (string-to-number (substring repeat 1)))))
(when (and repeat (not (zerop (string-to-number (substring repeat 1))))) (when (eq org-log-repeat t) (setq org-log-repeat 'state))
(when (eq org-log-repeat t) (setq org-log-repeat 'state)) (let ((to-state (or (org-entry-get nil "REPEAT_TO_STATE" 'selective)
(let ((to-state (or (org-entry-get nil "REPEAT_TO_STATE" 'selective) (and (stringp org-todo-repeat-to-state)
(and (stringp org-todo-repeat-to-state) org-todo-repeat-to-state)
org-todo-repeat-to-state) (and org-todo-repeat-to-state org-last-state))))
(and org-todo-repeat-to-state org-last-state)))) (org-todo (cond ((and to-state (member to-state org-todo-keywords-1))
(org-todo (cond to-state)
((and to-state (member to-state org-todo-keywords-1)) ((eq interpret 'type) org-last-state)
to-state) (head)
((eq interpret 'type) org-last-state) (t 'none))))
(head) (org-back-to-heading t)
(t 'none)))) (org-add-planning-info nil nil 'closed)
(org-back-to-heading t) ;; When `org-log-repeat' is non-nil or entry contains
(org-add-planning-info nil nil 'closed) ;; a clock, set LAST_REPEAT property.
;; When `org-log-repeat' is non-nil or entry contains (when (or org-log-repeat
;; a clock, set LAST_REPEAT property. (catch :clock
(when (or org-log-repeat (save-excursion
(catch :clock (while (re-search-forward org-clock-line-re end t)
(save-excursion (when (org-at-clock-log-p) (throw :clock t))))))
(while (re-search-forward org-clock-line-re end t) (org-entry-put nil "LAST_REPEAT" (format-time-string
(when (org-at-clock-log-p) (throw :clock t)))))) (org-time-stamp-format t t)
(org-entry-put nil "LAST_REPEAT" (format-time-string (current-time))))
(org-time-stamp-format t t) (when org-log-repeat
(current-time)))) (if (or (memq 'org-add-log-note (default-value 'post-command-hook))
(when org-log-repeat (memq 'org-add-log-note post-command-hook))
(if (or (memq 'org-add-log-note (default-value 'post-command-hook)) ;; We are already setup for some record.
(memq 'org-add-log-note post-command-hook)) (when (eq org-log-repeat 'note)
;; We are already setup for some record. ;; Make sure we take a note, not only a time stamp.
(when (eq org-log-repeat 'note) (setq org-log-note-how 'note))
;; Make sure we take a note, not only a time stamp. ;; Set up for taking a record.
(setq org-log-note-how 'note)) (org-add-log-setup 'state
;; Set up for taking a record. (or done-word (car org-done-keywords))
(org-add-log-setup 'state org-last-state
(or done-word (car org-done-keywords)) org-log-repeat)))
org-last-state (let ((planning-re (regexp-opt
org-log-repeat))) (list org-scheduled-string org-deadline-string))))
(let ((planning-re (regexp-opt (while (re-search-forward org-ts-regexp-both end t)
(list org-scheduled-string org-deadline-string)))) (let* ((ts (match-string 0))
(while (re-search-forward org-ts-regexp end t) (planning? (org-at-planning-p))
(let* ((ts (match-string 0)) (type (if (not planning?) "Plain:"
(planning? (org-at-planning-p)) (save-excursion
(type (if (not planning?) "Plain:" (re-search-backward
(save-excursion planning-re (line-beginning-position) t)
(re-search-backward (match-string 0)))))
planning-re (line-beginning-position) t) (cond
(match-string 0))))) ;; Ignore fake time-stamps (e.g., within comments).
(cond ((not (org-at-timestamp-p 'agenda)))
;; Ignore fake time-stamps (e.g., within comments). ((string-match "\\([.+]\\)?\\(\\+[0-9]+\\)\\([hdwmy]\\)" ts)
((not (org-at-timestamp-p 'agenda))) (let ((n (string-to-number (match-string 2 ts)))
;; Time-stamps without a repeater are usually (what (match-string 3 ts)))
;; skipped. However, a SCHEDULED time-stamp without (when (equal what "w") (setq n (* n 7) what "d"))
;; one is removed, as they are no longer relevant. (when (and (equal what "h")
((not (string-match "\\([.+]\\)?\\(\\+[0-9]+\\)\\([hdwmy]\\)" (not (string-match-p "[0-9]\\{1,2\\}:[0-9]\\{2\\}"
ts)) ts)))
(when (equal type org-scheduled-string) (user-error
(org-remove-timestamp-with-keyword type))) "Cannot repeat in %d hour(s) because no hour has been set"
(t n))
(let ((n (string-to-number (match-string 2 ts))) ;; Preparation, see if we need to modify the start
(what (match-string 3 ts))) ;; date for the change.
(when (equal what "w") (setq n (* n 7) what "d")) (when (match-end 1)
(when (and (equal what "h") (let ((time (save-match-data (org-time-string-to-time ts)))
(not (string-match-p "[0-9]\\{1,2\\}:[0-9]\\{2\\}" (repeater-type (match-string 1 ts)))
ts))) (cond
(user-error ((equal "." repeater-type)
"Cannot repeat in Repeat in %d hour(s) because no hour \ ;; Shift starting date to today.
has been set" (org-timestamp-change (- (org-today) (time-to-days time))
n)) 'day))
;; Preparation, see if we need to modify the start ((equal "+" repeater-type)
;; date for the change. (let ((nshiftmax 10)
(when (match-end 1) (nshift 0))
(let ((time (save-match-data (while (or (= nshift 0)
(org-time-string-to-time ts)))) (not (time-less-p (current-time) time)))
(cond (when (= nshiftmax (cl-incf nshift))
((equal (match-string 1 ts) ".") (or (y-or-n-p
;; Shift starting date to today (format "%d repeater intervals were not \
(org-timestamp-change
(- (org-today) (time-to-days time))
'day))
((equal (match-string 1 ts) "+")
(let ((nshiftmax 10)
(nshift 0))
(while (or (= nshift 0)
(not (time-less-p (current-time) time)))
(when (= (cl-incf nshift) nshiftmax)
(or (y-or-n-p
(format "%d repeater intervals were not \
enough to shift date past today. Continue? " enough to shift date past today. Continue? "
nshift)) nshift))
(user-error "Abort"))) (user-error "Abort")))
(org-timestamp-change n (cdr (assoc what whata))) (org-timestamp-change n (cdr (assoc what whata)))
(org-in-regexp org-ts-regexp3)
(setq ts (match-string 1))
(setq time
(save-match-data
(org-time-string-to-time ts)))))
(org-timestamp-change (- n) (cdr (assoc what whata)))
;; Rematch, so that we have everything in place
;; for the real shift.
(org-in-regexp org-ts-regexp3) (org-in-regexp org-ts-regexp3)
(setq ts (match-string 1)) (setq ts (match-string 1))
(string-match "\\([.+]\\)?\\(\\+[0-9]+\\)\\([hdwmy]\\)" (setq time
ts))))) (save-match-data
(save-excursion (org-time-string-to-time ts)))))
(org-timestamp-change n (cdr (assoc what whata)) nil t)) (org-timestamp-change (- n) (cdr (assoc what whata)))
(setq msg ;; Rematch, so that we have everything in place
(concat ;; for the real shift.
msg type " " org-last-changed-timestamp " ")))))))) (org-in-regexp org-ts-regexp3)
(setq org-log-post-message msg) (setq ts (match-string 1))
(message "%s" msg)) (string-match "\\([.+]\\)?\\(\\+[0-9]+\\)\\([hdwmy]\\)"
(set-marker end nil)))) ts)))))
(save-excursion
(org-timestamp-change n (cdr (assoc what whata)) nil t))
(setq msg
(concat msg type " " org-last-changed-timestamp " "))))
(t
;; Time-stamps without a repeater are usually skipped.
;; However, a SCHEDULED time-stamp without one is
;; removed, as they are no longer relevant.
(when (equal type org-scheduled-string)
(org-remove-timestamp-with-keyword type)))))))
(setq org-log-post-message msg)
(message "%s" msg))))
(defun org-show-todo-tree (arg) (defun org-show-todo-tree (arg)
"Make a compact tree which shows all headlines marked with TODO. "Make a compact tree which shows all headlines marked with TODO.

View File

@ -6607,6 +6607,15 @@ Paragraph<point>"
(org-test-with-temp-text "* TODO H\n<2012-03-29 Thu +2h>" (org-test-with-temp-text "* TODO H\n<2012-03-29 Thu +2h>"
(org-todo "DONE") (org-todo "DONE")
(buffer-string)))) (buffer-string))))
;; Also repeat inactive time stamps with a repeater.
(should
(string-match-p
"\\[2014-03-29 .* \\+2y\\]"
(let ((org-todo-keywords '((sequence "TODO" "DONE"))))
(org-test-with-temp-text
"* TODO H\n[2012-03-29 Thu. +2y]"
(org-todo "DONE")
(buffer-string)))))
;; Do not repeat commented time stamps. ;; Do not repeat commented time stamps.
(should-not (should-not
(string-prefix-p (string-prefix-p