]> code.delx.au - gnu-emacs/blobdiff - lisp/calendar/todo-mode.el
Fix todo-mode item date editing bugs
[gnu-emacs] / lisp / calendar / todo-mode.el
index e5f486a2056cdfe266c04961a8e70a5abb29c14a..94cd08eaa4e898f8e35de3bac942212cb3583167 100644 (file)
@@ -1,6 +1,6 @@
 ;;; todo-mode.el --- facilities for making and maintaining todo lists
 
-;; Copyright (C) 1997, 1999, 2001-2014 Free Software Foundation, Inc.
+;; Copyright (C) 1997, 1999, 2001-2016 Free Software Foundation, Inc.
 
 ;; Author: Oliver Seidel <privat@os10000.net>
 ;;     Stephen Berman <stephen.berman@gmx.net>
@@ -74,7 +74,7 @@ truenames (those with the extension \".toda\")."
   (let ((files (if (file-exists-p todo-directory)
                   (mapcar 'file-truename
                    (directory-files todo-directory t
-                                    (if archives "\.toda$" "\.todo$") t)))))
+                                    (if archives "\\.toda$" "\\.todo$") t)))))
     (sort files (lambda (s1 s2) (let ((cis1 (upcase s1))
                                      (cis2 (upcase s2)))
                                  (string< cis1 cis2))))))
@@ -237,7 +237,8 @@ The final element is \"*\", indicating an unspecified month.")
                   (when (string= (widget-value widget) todo-item-mark)
                     (widget-put
                      widget :error
-                     "Invalid value: must be distinct from `todo-item-mark'")
+                     (format-message
+                      "Invalid value: must be distinct from `todo-item-mark'"))
                     widget)))
   :initialize 'custom-initialize-default
   :set 'todo-reset-prefix
@@ -672,7 +673,7 @@ corresponding todo file, displaying the corresponding category."
                                                      todo-filtered-items-mode))))
                          (if (funcall todo-files-function)
                              (todo-read-file-name "Choose a todo file to visit: "
-                                                   nil t)
+                                                  nil t)
                            (user-error "There are no todo files")))
                         ((and (eq major-mode 'todo-archive-mode)
                               ;; Called noninteractively via todo-quit
@@ -732,7 +733,10 @@ corresponding todo file, displaying the corresponding category."
        (when (or (member file todo-visited)
                  (eq todo-show-first 'first))
          (unless (todo-check-file file) (throw 'end nil))
-         (set-window-buffer (selected-window)
+          ;; If todo-show is called from the minibuffer, don't visit
+          ;; the todo file there.
+         (set-window-buffer (if (minibufferp) (minibuffer-selected-window)
+                              (selected-window))
                             (set-buffer (find-file-noselect file 'nowarn)))
          (if (equal (file-name-extension (buffer-file-name)) "toda")
              (unless (derived-mode-p 'todo-archive-mode) (todo-archive-mode))
@@ -743,6 +747,11 @@ corresponding todo file, displaying the corresponding category."
            (setq todo-category-number (todo-category-number cat)))
          ;; If this is a new todo file, add its first category.
          (when (zerop (buffer-size))
+            ;; Don't confuse an erased buffer with a fresh buffer for
+            ;; adding a new todo file -- it might have been erased by
+            ;; mistake or due to a bug (e.g. Bug#20832).
+            (when (buffer-modified-p)
+              (error "Buffer is empty but modified, please report a bug"))
            (let (cat-added)
              (unwind-protect
                  (setq todo-category-number
@@ -1103,7 +1112,7 @@ these files, also rename them accordingly."
         (snname (todo-short-file-name nname))
         (files (directory-files todo-directory t
                                 (concat ".*" (regexp-quote soname)
-                                        ".*\.tod[aorty]$") t)))
+                                        ".*\\.tod[aorty]$") t)))
     (dolist (f files)
       (let* ((sfname (todo-short-file-name f))
             (fext (file-name-extension f t))
@@ -1334,12 +1343,13 @@ todo or done items."
                            "deleting it will also delete the file.\n"
                            "Do you want to proceed? ")))
                  ((> archived 0)
-                  (todo-y-or-n-p (concat "This category has archived items; "
+                  (todo-y-or-n-p (format-message
+                                  (concat "This category has archived items; "
                                     "the archived category will remain\n"
                                     "after deleting the todo category.  "
                                     "Do you still want to delete it\n"
                                     "(see `todo-skip-archived-categories' "
-                                    "for another option)? ")))
+                                    "for another option)? "))))
                  (t
                   (todo-y-or-n-p (concat "Permanently remove category \"" cat
                                     "\"" (and arg " and all its entries")
@@ -1686,7 +1696,8 @@ only when no items are marked."
                   (when (string= (widget-value widget) todo-prefix)
                     (widget-put
                      widget :error
-                     "Invalid value: must be distinct from `todo-prefix'")
+                     (format-message
+                      "Invalid value: must be distinct from `todo-prefix'"))
                     widget)))
   :set (lambda (symbol value)
         (custom-set-default symbol (propertize value 'face 'todo-mark)))
@@ -2068,85 +2079,101 @@ the item at point."
 (defun todo-edit-item (&optional arg)
   "Choose an editing operation for the current item and carry it out."
   (interactive "P")
-  (cond ((todo-done-item-p)
-        (todo-edit-item--next-key todo-edit-done-item--param-key-alist))
-       ((todo-item-string)
-        (todo-edit-item--next-key todo-edit-item--param-key-alist arg))))
+  (let ((marked (assoc (todo-current-category) todo-categories-with-marks)))
+    (cond ((and (todo-done-item-p) (not marked))
+          (todo-edit-item--next-key todo-edit-done-item--param-key-alist))
+         ((or marked (todo-item-string))
+          (todo-edit-item--next-key todo-edit-item--param-key-alist arg)))))
 
 (defun todo-edit-item--text (&optional arg)
   "Function providing the text editing facilities of `todo-edit-item'."
-  (let* ((opoint (point))
-        (start (todo-item-start))
-        (end (save-excursion (todo-item-end)))
-        (item-beg (progn
-                    (re-search-forward
-                     (concat todo-date-string-start todo-date-pattern
-                             "\\( " diary-time-regexp "\\)?"
-                             (regexp-quote todo-nondiary-end) "?")
-                     (line-end-position) t)
-                    (1+ (- (point) start))))
-        (include-header (eq arg 'include-header))
-        (comment-edit (eq arg 'comment-edit))
-        (comment-delete (eq arg 'comment-delete))
-        (header-string (substring (todo-item-string) 0 item-beg))
-        (item (if (or include-header comment-edit comment-delete)
-                  (todo-item-string)
-                (substring (todo-item-string) item-beg)))
-        (multiline (> (length (split-string item "\n")) 1))
-        (comment (save-excursion
-                   (todo-item-start)
-                   (re-search-forward
-                    (concat " \\[" (regexp-quote todo-comment-string)
-                            ": \\([^]]+\\)\\]") end t)))
-        (prompt (if comment "Edit comment: " "Enter a comment: "))
-        (buffer-read-only nil))
-    (cond
-     ((or comment-edit comment-delete)
-      (save-excursion
-       (todo-item-start)
-       (if (re-search-forward (concat " \\[" (regexp-quote todo-comment-string)
-                                      ": \\([^]]+\\)\\]") end t)
-           (if comment-delete
-               (when (todo-y-or-n-p "Delete comment? ")
-                 (delete-region (match-beginning 0) (match-end 0)))
-             (replace-match (read-string prompt (cons (match-string 1) 1))
-                            nil nil nil 1))
-         (if comment-delete
-             (user-error "There is no comment to delete")
-           (insert " [" todo-comment-string ": "
-                   (prog1 (read-string prompt)
-                     ;; If user moved point during editing,
-                     ;; make sure it moves back.
-                     (goto-char opoint)
-                     (todo-item-end))
-                     "]")))))
-     ((or multiline (eq arg 'multiline))
-      (let ((buf todo-edit-buffer))
-       (set-window-buffer (selected-window)
-                          (set-buffer (make-indirect-buffer (buffer-name) buf)))
-       (narrow-to-region (todo-item-start) (todo-item-end))
-       (todo-edit-mode)
-       (message "%s" (substitute-command-keys
-                      (concat "Type \\[todo-edit-quit] "
-                              "to return to Todo mode.\n")))))
-     (t
-      (let ((new (concat (if include-header "" header-string)
-                         (read-string "Edit: " (if include-header
-                                                   (cons item item-beg)
-                                                 (cons item 0))))))
-        (when include-header
-          (while (not (string-match (concat todo-date-string-start
-                                            todo-date-pattern) new))
-            (setq new (read-from-minibuffer
-                       "Item must start with a date: " new))))
-        ;; Ensure lines following hard newlines are indented.
-        (setq new (replace-regexp-in-string "\\(\n\\)[^[:blank:]]"
-                                            "\n\t" new nil nil 1))
-        ;; If user moved point during editing, make sure it moves back.
-        (goto-char opoint)
-        (todo-remove-item)
-        (todo-insert-with-overlays new)
-        (move-to-column item-beg))))))
+  (let ((full-item (todo-item-string)))
+    ;; If there are marked items and user invokes a text-editing
+    ;; commands with point not on an item, todo-item-start is nil and
+    ;; 1+ signals an error, so just make this a noop.
+    (when full-item
+      (let* ((opoint (point))
+            (start (todo-item-start))
+            (end (save-excursion (todo-item-end)))
+            (item-beg (progn
+                        (re-search-forward
+                         (concat todo-date-string-start todo-date-pattern
+                                 "\\( " diary-time-regexp "\\)?"
+                                 (regexp-quote todo-nondiary-end) "?")
+                         (line-end-position) t)
+                        (1+ (- (point) start))))
+            (include-header (eq arg 'include-header))
+            (comment-edit (eq arg 'comment-edit))
+            (comment-delete (eq arg 'comment-delete))
+            (header-string (substring full-item 0 item-beg))
+            (item (if (or include-header comment-edit comment-delete)
+                      full-item
+                    (substring full-item item-beg)))
+            (multiline (or (eq arg 'multiline)
+                           (> (length (split-string item "\n")) 1)))
+            (comment (save-excursion
+                       (todo-item-start)
+                       (re-search-forward
+                        (concat " \\[" (regexp-quote todo-comment-string)
+                                ": \\([^]]+\\)\\]") end t)))
+            (prompt (if comment "Edit comment: " "Enter a comment: "))
+            (buffer-read-only nil))
+       ;; When there are marked items, user can invoke todo-edit-item
+       ;; even if point is not on an item, but text editing only
+       ;; applies to the item at point.
+       (when (or (and (todo-done-item-p)
+                      (or comment-edit comment-delete))
+                 (and (not (todo-done-item-p))
+                      (or (not arg) include-header multiline)))
+         (cond
+          ((or comment-edit comment-delete)
+           (save-excursion
+             (todo-item-start)
+             (if (re-search-forward (concat " \\["
+                                            (regexp-quote todo-comment-string)
+                                            ": \\([^]]+\\)\\]") end t)
+                 (if comment-delete
+                     (when (todo-y-or-n-p "Delete comment? ")
+                       (delete-region (match-beginning 0) (match-end 0)))
+                   (replace-match (read-string prompt (cons (match-string 1) 1))
+                                  nil nil nil 1))
+               (if comment-delete
+                   (user-error "There is no comment to delete")
+                 (insert " [" todo-comment-string ": "
+                         (prog1 (read-string prompt)
+                           ;; If user moved point during editing,
+                           ;; make sure it moves back.
+                           (goto-char opoint)
+                           (todo-item-end))
+                         "]")))))
+          (multiline
+           (let ((buf todo-edit-buffer))
+             (set-window-buffer (selected-window)
+                                (set-buffer (make-indirect-buffer
+                                             (buffer-name) buf)))
+             (narrow-to-region (todo-item-start) (todo-item-end))
+             (todo-edit-mode)
+             (message "%s" (substitute-command-keys
+                            (concat "Type \\[todo-edit-quit] "
+                                    "to return to Todo mode.\n")))))
+          (t
+           (let ((new (concat (if include-header "" header-string)
+                              (read-string "Edit: " (if include-header
+                                                        (cons item item-beg)
+                                                      (cons item 0))))))
+             (when include-header
+               (while (not (string-match (concat todo-date-string-start
+                                                 todo-date-pattern) new))
+                 (setq new (read-from-minibuffer
+                            "Item must start with a date: " new))))
+             ;; Ensure lines following hard newlines are indented.
+             (setq new (replace-regexp-in-string "\\(\n\\)[^[:blank:]]"
+                                                 "\n\t" new nil nil 1))
+             ;; If user moved point during editing, make sure it moves back.
+             (goto-char opoint)
+             (todo-remove-item)
+             (todo-insert-with-overlays new)
+             (move-to-column item-beg)))))))))
 
 (defun todo-edit-quit ()
   "Return from Todo Edit mode to Todo mode.
@@ -2201,16 +2228,16 @@ made in the number or names of categories."
 
 (defun todo-edit-item--header (what &optional inc)
   "Function providing header editing facilities of `todo-edit-item'."
-  (let* ((cat (todo-current-category))
-        (marked (assoc cat todo-categories-with-marks))
-        (first t)
-        (todo-date-from-calendar t)
-        ;; INC must be an integer, but users could pass it via
-        ;; `todo-edit-item' as e.g. `-' or `C-u'.
-        (inc (prefix-numeric-value inc))
-        (buffer-read-only nil)
-        ndate ntime year monthname month day
-        dayname)       ; Needed by calendar-date-display-form.
+  (let ((marked (assoc (todo-current-category) todo-categories-with-marks))
+       (first t)
+       (todo-date-from-calendar t)
+       ;; INC must be an integer, but users could pass it via
+       ;; `todo-edit-item' as e.g. `-' or `C-u'.
+       (inc (prefix-numeric-value inc))
+       (buffer-read-only nil)
+       ndate ntime year monthname month day
+       dayname)        ; Needed by calendar-date-display-form.
+    (when marked (todo--user-error-if-marked-done-item))
     (save-excursion
       (or (and marked (goto-char (point-min))) (todo-item-start))
       (catch 'end
@@ -2235,9 +2262,8 @@ made in the number or names of categories."
                 (mlist (append tmn-array nil))
                 (tma-array todo-month-abbrev-array)
                 (mablist (append tma-array nil))
-                (yy (and oyear (unless (string= oyear "*")
-                                 (string-to-number oyear))))
-                (mm (or (and omonth (unless (string= omonth "*")
+                (yy (and oyear (string-to-number oyear))) ; 0 if year is "*".
+                (mm (or (and omonth (if (string= omonth "*") 13
                                       (string-to-number omonth)))
                         (1+ (- (length mlist)
                                (length (or (member omonthname mlist)
@@ -2303,12 +2329,11 @@ made in the number or names of categories."
                             (if omonth
                                 (number-to-string mm)
                               (aref tma-array (1- mm))))))
-               (let ((yy (string-to-number year)) ; 0 if year is "*".
-                     ;; When mm is 13 (corresponding to "*" as value
-                     ;; of month), this raises an args-out-of-range
-                     ;; error in calendar-last-day-of-month, so use 1
-                     ;; (corresponding to January) to get 31 days.
-                     (mm (if (= mm 13) 1 mm)))
+                ;; Since the number corresponding to the arbitrary
+                ;; month name "*" is out of the range of
+                ;; calendar-last-day-of-month, set it to 1
+                ;; (corresponding to January) to allow 31 days.
+                (let ((mm (if (= mm 13) 1 mm)))
                  (if (> (string-to-number day)
                         (calendar-last-day-of-month mm yy))
                      (user-error "%s %s does not have %s days"
@@ -2320,7 +2345,7 @@ made in the number or names of categories."
                      monthname omonthname
                      day (cond
                           ((not current-prefix-arg)
-                           (todo-read-date 'day mm oyear))
+                           (todo-read-date 'day mm yy))
                           ((string= oday "*")
                            (user-error "Cannot increment *"))
                           ((or (string= omonth "*") (string= omonthname "*"))
@@ -2372,47 +2397,45 @@ made in the number or names of categories."
 (defun todo-edit-item--diary-inclusion (&optional nonmarking)
   "Function providing diary marking facilities of `todo-edit-item'."
   (let ((buffer-read-only)
-       (marked (assoc (todo-current-category)
-                      todo-categories-with-marks)))
+       (marked (assoc (todo-current-category) todo-categories-with-marks)))
+    (when marked (todo--user-error-if-marked-done-item))
     (catch 'stop
       (save-excursion
        (when marked (goto-char (point-min)))
        (while (not (eobp))
-         (if (todo-done-item-p)
-             (throw 'stop (message "Done items cannot be edited"))
-           (unless (and marked (not (todo-marked-item-p)))
-             (let* ((beg (todo-item-start))
-                    (lim (save-excursion (todo-item-end)))
-                    (end (save-excursion
-                           (or (todo-time-string-matcher lim)
-                               (todo-date-string-matcher lim)))))
-               (if nonmarking
-                   (if (looking-at (regexp-quote diary-nonmarking-symbol))
-                       (replace-match "")
-                     (when (looking-at (regexp-quote todo-nondiary-start))
-                       (save-excursion
-                         (replace-match "")
-                         (search-forward todo-nondiary-end (1+ end) t)
-                         (replace-match "")
-                         (todo-update-count 'diary 1)))
-                     (insert diary-nonmarking-symbol))
-                 (if (looking-at (regexp-quote todo-nondiary-start))
-                     (progn
+         (unless (and marked (not (todo-marked-item-p)))
+           (let* ((beg (todo-item-start))
+                  (lim (save-excursion (todo-item-end)))
+                  (end (save-excursion
+                         (or (todo-time-string-matcher lim)
+                             (todo-date-string-matcher lim)))))
+             (if nonmarking
+                 (if (looking-at (regexp-quote diary-nonmarking-symbol))
+                     (replace-match "")
+                   (when (looking-at (regexp-quote todo-nondiary-start))
+                     (save-excursion
                        (replace-match "")
                        (search-forward todo-nondiary-end (1+ end) t)
                        (replace-match "")
-                       (todo-update-count 'diary 1))
-                   (when end
-                     (when (looking-at (regexp-quote diary-nonmarking-symbol))
-                       (replace-match "")
-                       (setq end (1- end))) ; Since we deleted nonmarking symbol.
-                     (insert todo-nondiary-start)
-                     (goto-char (1+ end))
-                     (insert todo-nondiary-end)
-                     (todo-update-count 'diary -1))))))
-           (unless marked (throw 'stop nil))
-           (todo-forward-item)))))
-    (todo-update-categories-sexp)))
+                       (todo-update-count 'diary 1)))
+                   (insert diary-nonmarking-symbol))
+               (if (looking-at (regexp-quote todo-nondiary-start))
+                   (progn
+                     (replace-match "")
+                     (search-forward todo-nondiary-end (1+ end) t)
+                     (replace-match "")
+                     (todo-update-count 'diary 1))
+                 (when end
+                   (when (looking-at (regexp-quote diary-nonmarking-symbol))
+                     (replace-match "")
+                     (setq end (1- end))) ; Since we deleted nonmarking symbol.
+                   (insert todo-nondiary-start)
+                   (goto-char (1+ end))
+                   (insert todo-nondiary-end)
+                   (todo-update-count 'diary -1))))))
+         (unless marked (throw 'stop nil))
+         (todo-forward-item)))))
+  (todo-update-categories-sexp))
 
 (defun todo-edit-category-diary-inclusion (arg)
   "Make all items in this category diary items.
@@ -2592,7 +2615,8 @@ meaning to raise or lower the item's priority by one."
            ;; separator.
            (when (looking-back (concat "^"
                                        (regexp-quote todo-category-done)
-                                       "\n"))
+                                       "\n")
+                                (line-beginning-position 0))
              (todo-backward-item))))
        (todo-insert-with-overlays item)
        ;; If item was marked, restore the mark.
@@ -2783,21 +2807,7 @@ visible."
   (interactive "P")
   (let* ((cat (todo-current-category))
         (marked (assoc cat todo-categories-with-marks)))
-    (when marked
-      (save-excursion
-       (save-restriction
-         (goto-char (point-max))
-         (todo-backward-item)
-         (unless (todo-done-item-p)
-           (widen)
-           (unless (re-search-forward
-                    (concat "^" (regexp-quote todo-category-beg)) nil t)
-             (goto-char (point-max)))
-           (forward-line -1))
-         (while (todo-done-item-p)
-           (when (todo-marked-item-p)
-             (user-error "This command does not apply to done items"))
-           (todo-backward-item)))))
+    (when marked (todo--user-error-if-marked-done-item))
     (unless (and (not marked)
                 (or (todo-done-item-p)
                     ;; Point is between todo and done items.
@@ -2816,7 +2826,8 @@ visible."
                          (goto-char (point-min))
                          (re-search-forward todo-done-string-start nil t)))
             (buffer-read-only nil)
-            item done-item opoint)
+            item done-item
+            (opoint (point)))
        ;; Don't add empty comment to done item.
        (setq comment (unless (zerop (length comment))
                        (concat " [" todo-comment-string ": " comment "]")))
@@ -2854,7 +2865,9 @@ visible."
        (todo-update-categories-sexp)
        (let ((todo-show-with-done show-done))
          (todo-category-select)
-         ;; When done items are shown, put cursor on first just done item.
+         ;; When done items are visible, put point at the top of the
+         ;; done items section.  When done items are hidden, restore
+         ;; point to its location prior to invoking this command.
          (when opoint (goto-char opoint)))))))
 
 (defun todo-item-undone ()
@@ -2885,7 +2898,9 @@ comments without asking."
          (while (not (eobp))
            (when (or (not marked) (and marked (todo-marked-item-p)))
              (if (not (todo-done-item-p))
-                 (user-error "Only done items can be undone")
+                 (progn
+                   (goto-char opoint)
+                   (user-error "Only done items can be undone"))
                (todo-item-start)
                (unless marked
                  (setq ov (make-overlay (save-excursion (todo-item-start))
@@ -3946,7 +3961,7 @@ regexp items."
 (defun todo-find-filtered-items-file ()
   "Choose a filtered items file and visit it."
   (interactive)
-  (let ((files (directory-files todo-directory t "\.tod[rty]$" t))
+  (let ((files (directory-files todo-directory t "\\.tod[rty]$" t))
        falist file)
     (dolist (f files)
       (let ((type (cond ((equal (file-name-extension f) "todr") "regexp")
@@ -4226,7 +4241,8 @@ the values of FILTER and FILE-LIST."
                           (if (and (eobp)
                                    (looking-back
                                     (concat (regexp-quote todo-done-string)
-                                            "\n")))
+                                            "\n")
+                                     (line-beginning-position 0)))
                               (delete-region (point) (progn
                                                        (forward-line -2)
                                                        (point))))))
@@ -4636,14 +4652,16 @@ name in `todo-directory'.  See also the documentation string of
                    (goto-char (match-beginning 0))
                  (goto-char (point-max)))
                (backward-char)
-               (when (looking-back "\\[\\([^][]+\\)\\]")
+               (when (looking-back "\\[\\([^][]+\\)\\]"
+                                    (line-beginning-position))
                  (setq cat (match-string 1))
                  (goto-char (match-beginning 0))
                  (replace-match ""))
                ;; If the item ends with a non-comment parenthesis not
                ;; followed by a period, we lose (but we inherit that
                ;; problem from the legacy code).
-               (when (looking-back "(\\(.*\\)) ")
+                ;; FIXME: fails on multiline comment
+               (when (looking-back "(\\(.*\\)) " (line-beginning-position))
                  (setq comment (match-string 1))
                  (replace-match "")
                  (insert "[" todo-comment-string ": " comment "]"))
@@ -4874,7 +4892,7 @@ With nil or omitted CATEGORY, default to the current category."
        (widen)
        (goto-char (point-min))
        (setq todo-categories
-             (if (looking-at "\(\(\"")
+             (if (looking-at "((\"")
                  (read (buffer-substring-no-properties
                         (line-beginning-position)
                         (line-end-position)))
@@ -5019,7 +5037,7 @@ but the categories sexp differs from the current value of
        ;; Warn user if categories sexp has changed.
        (unless (string= ssexp cats)
          (message (concat "The sexp at the beginning of the file differs "
-                          "from the value of `todo-categories.\n"
+                          "from the value of `todo-categories'.\n"
                           "If the sexp is wrong, you can fix it with "
                           "M-x todo-repair-categories-sexp,\n"
                           "but note this reverts any changes you have "
@@ -5192,6 +5210,15 @@ Overrides `diary-goto-entry'."
 
 (add-function :override diary-goto-entry-function #'todo-diary-goto-entry)
 
+(defun todo-revert-buffer (&optional ignore-auto noconfirm)
+  "Call `revert-buffer', preserving buffer's current modes.
+Also preserve category display, if applicable."
+  (interactive (list (not current-prefix-arg)))
+  (let ((revert-buffer-function nil))
+    (revert-buffer ignore-auto noconfirm 'preserve-modes)
+    (when (memq major-mode '(todo-mode todo-archive-mode))
+      (todo-category-select))))
+
 (defun todo-desktop-save-buffer (_dir)
   `((catnum . ,(todo-category-number (todo-current-category)))))
 
@@ -5203,7 +5230,8 @@ Overrides `diary-goto-entry'."
   (with-current-buffer buffer
     (widen)
     (let ((todo-category-number (cdr (assq 'catnum misc))))
-      (todo-category-select))))
+      (todo-category-select)
+      (current-buffer))))
 
 (add-to-list 'desktop-buffer-mode-handlers
             '(todo-mode . todo-restore-desktop-buffer))
@@ -5222,6 +5250,25 @@ Overrides `diary-goto-entry'."
        (progn (goto-char (point-min))
               (looking-at todo-done-string-start)))))
 
+(defun todo--user-error-if-marked-done-item ()
+  "Signal user error on marked done items.
+Helper function for editing commands that apply only to (possibly
+marked) not done todo items."
+  (save-excursion
+    (save-restriction
+      (goto-char (point-max))
+      (todo-backward-item)
+      (unless (todo-done-item-p)
+       (widen)
+       (unless (re-search-forward
+                (concat "^" (regexp-quote todo-category-beg)) nil t)
+         (goto-char (point-max)))
+       (forward-line -1))
+      (while (todo-done-item-p)
+       (when (todo-marked-item-p)
+         (user-error "This command does not apply to done items"))
+       (todo-backward-item)))))
+
 (defun todo-reset-done-separator (sep)
   "Replace existing overlays of done items separator string SEP."
   (save-excursion
@@ -5309,7 +5356,8 @@ of each other."
                     (looking-at todo-done-string-start)
                     (looking-back (concat "^"
                                           (regexp-quote todo-category-done)
-                                          "\n")))
+                                          "\n")
+                                   (line-beginning-position 0)))
            (setq num 1
                  done t))
          (setq prefix (concat (propertize
@@ -5407,7 +5455,7 @@ dynamically create item insertion commands.")
 The list consists of item insertion parameters that can be passed
 as insertion command arguments in fixed positions.  If a position
 in the list is not occupied by the corresponding parameter, it is
-occupied by `nil'."
+occupied by nil."
   (let* ((arg (list (car todo-insert-item--args)))
         (args (nconc (cdr todo-insert-item--args)
                      (list (car (todo-insert-item--argsleft
@@ -5530,8 +5578,9 @@ already entered and those still available."
                                                        '(add/edit delete))
                                              " comment"))))
                          params " "))
-        (this-key (char-to-string
-                   (read-key (concat todo-edit-item--prompt p->k))))
+        (key-prompt (substitute-command-keys todo-edit-item--prompt))
+        (this-key (let ((key (read-key (concat key-prompt p->k))))
+                    (and (characterp key) (char-to-string key))))
         (this-param (car (rassoc this-key params))))
     (pcase this-param
       (`edit (todo-edit-item--text))
@@ -5595,9 +5644,10 @@ have been removed."
     (when deleted
       (let ((pl (> (length deleted) 1))
            (names (mapconcat (lambda (f) (concat "\"" f "\"")) deleted ", ")))
-       (message (concat "File" (if pl "s" "") " " names " ha" (if pl "ve" "s")
+       (message (concat "File" (if pl "s" "") " %s ha" (if pl "ve" "s")
                         " been deleted and removed from\n"
-                        "the list of category completion files")))
+                        "the list of category completion files")
+                names))
       (todo-reevaluate-category-completions-files-defcustom)
       (custom-set-default 'todo-category-completions-files
                          (symbol-value 'todo-category-completions-files))
@@ -5881,7 +5931,7 @@ number of the last the day of the month."
     (and day (setq day (if (eq day '*)
                           (symbol-name '*)
                         (number-to-string day))))
-    (and month (setq month (if (eq month '*)
+    (and month (setq month (if (= month 13)
                               (symbol-name '*)
                             (number-to-string month))))
     (if arg
@@ -5974,7 +6024,7 @@ the empty string (i.e., no time string)."
   "The :set function for user option `todo-nondiary-marker'."
   (let* ((oldvalue (symbol-value symbol))
         (files (append todo-files todo-archives
-                       (directory-files todo-directory t "\.tod[rty]$" t))))
+                       (directory-files todo-directory t "\\.tod[rty]$" t))))
     (custom-set-default symbol value)
     ;; Need to reset these to get font-locking right.
     (setq todo-nondiary-start (nth 0 todo-nondiary-marker)
@@ -6027,7 +6077,7 @@ the empty string (i.e., no time string)."
   "The :set function for user option `todo-done-string'."
   (let ((oldvalue (symbol-value symbol))
        (files (append todo-files todo-archives
-                      (directory-files todo-directory t "\.todr$" t))))
+                      (directory-files todo-directory t "\\.todr$" t))))
     (custom-set-default symbol value)
     ;; Need to reset this to get font-locking right.
     (setq todo-done-string-start
@@ -6056,7 +6106,7 @@ the empty string (i.e., no time string)."
   "The :set function for user option `todo-comment-string'."
   (let ((oldvalue (symbol-value symbol))
        (files (append todo-files todo-archives
-                      (directory-files todo-directory t "\.todr$" t))))
+                      (directory-files todo-directory t "\\.todr$" t))))
     (custom-set-default symbol value)
     (when (not (equal value oldvalue))
       (dolist (f files)
@@ -6082,7 +6132,7 @@ the empty string (i.e., no time string)."
   "The :set function for user option `todo-highlight-item'."
   (let ((oldvalue (symbol-value symbol))
        (files (append todo-files todo-archives
-                      (directory-files todo-directory t "\.tod[rty]$" t))))
+                      (directory-files todo-directory t "\\.tod[rty]$" t))))
     (custom-set-default symbol value)
     (when (not (equal value oldvalue))
       (dolist (f files)
@@ -6519,6 +6569,7 @@ Added to `window-configuration-change-hook' in Todo mode."
 (defun todo-modes-set-1 ()
   "Make some settings that apply to multiple Todo modes."
   (setq-local font-lock-defaults '(todo-font-lock-keywords t))
+  (setq-local revert-buffer-function 'todo-revert-buffer)
   (setq-local tab-width todo-indent-to-here)
   (setq-local indent-line-function 'todo-indent)
   (when todo-wrap-lines
@@ -6529,8 +6580,7 @@ Added to `window-configuration-change-hook' in Todo mode."
   "Make some settings that apply to multiple Todo modes."
   (add-to-invisibility-spec 'todo)
   (setq buffer-read-only t)
-  (when (and (boundp 'desktop-save-mode) desktop-save-mode)
-    (setq-local desktop-save-buffer 'todo-desktop-save-buffer))
+  (setq-local desktop-save-buffer 'todo-desktop-save-buffer)
   (when (boundp 'hl-line-range-function)
     (setq-local hl-line-range-function
                (lambda() (save-excursion
@@ -6552,23 +6602,26 @@ Added to `window-configuration-change-hook' in Todo mode."
   "Major mode for displaying, navigating and editing todo lists.
 
 \\{todo-mode-map}"
-  ;; (easy-menu-add todo-menu)
-  (todo-modes-set-1)
-  (todo-modes-set-2)
-  (todo-modes-set-3)
-  ;; Initialize todo-current-todo-file.
-  (when (member (file-truename (buffer-file-name))
-               (funcall todo-files-function))
-    (setq-local todo-current-todo-file (file-truename (buffer-file-name))))
-  (setq-local todo-show-done-only nil)
-  (setq-local todo-categories-with-marks nil)
-  ;; (add-hook 'find-file-hook 'todo-add-to-buffer-list nil t)
-  (add-hook 'post-command-hook 'todo-update-buffer-list nil t)
-  (when todo-show-current-file
-    (add-hook 'pre-command-hook 'todo-show-current-file nil t))
-  (add-hook 'window-configuration-change-hook
-           'todo-reset-and-enable-done-separator nil t)
-  (add-hook 'kill-buffer-hook 'todo-reset-global-current-todo-file nil t))
+  (if (called-interactively-p 'any)
+      (message "%s"
+               (substitute-command-keys
+                "Type `\\[todo-show]' to enter Todo mode"))
+    (todo-modes-set-1)
+    (todo-modes-set-2)
+    (todo-modes-set-3)
+    ;; Initialize todo-current-todo-file.
+    (when (member (file-truename (buffer-file-name))
+                 (funcall todo-files-function))
+      (setq-local todo-current-todo-file (file-truename (buffer-file-name))))
+    (setq-local todo-show-done-only nil)
+    (setq-local todo-categories-with-marks nil)
+    ;; (add-hook 'find-file-hook 'todo-add-to-buffer-list nil t)
+    (add-hook 'post-command-hook 'todo-update-buffer-list nil t)
+    (when todo-show-current-file
+      (add-hook 'pre-command-hook 'todo-show-current-file nil t))
+    (add-hook 'window-configuration-change-hook
+             'todo-reset-and-enable-done-separator nil t)
+    (add-hook 'kill-buffer-hook 'todo-reset-global-current-todo-file nil t)))
 
 (put 'todo-archive-mode 'mode-class 'special)