]> 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 759b77737139e5496baa8f30d120e2fda57ccf5a..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>
 
 ;;; Commentary:
 
-;; This package provides facilities for making, displaying, navigating
-;; and editing todo lists, which are prioritized lists of todo items.
-;; Todo lists are identified with named categories, so you can group
-;; together and separately prioritize thematically related todo items.
-;; Each category is stored in a file, which thus provides a further
-;; level of organization.  You can create as many todo files, and in
-;; each as many categories, as you want.
+;; This package provides facilities for making and maintaining
+;; prioritized lists of things to do.  These todo lists are identified
+;; with named categories, so you can group together thematically
+;; related todo items.  Each category is stored in a file, providing a
+;; further level of organization.  You can create as many todo files,
+;; and in each as many categories, as you want.
 
 ;; With Todo mode you can navigate among the items of a category, and
 ;; between categories in the same and in different todo files.  You
-;; can edit todo items, reprioritize them within their category, move
-;; them to another category, delete them, or mark items as done and
-;; store them separately from the not yet done items in a category.
-;; You can add new todo files, edit and delete them.  You can add new
-;; categories, rename and delete them, move categories to another file
-;; and merge the items of two categories.  You can also reorder the
-;; sequence of categories in a todo file for the purpose of
-;; navigation.  You can display summary tables of the categories in a
-;; file and the types of items they contain.  And you can compile
-;; lists of existing items from multiple categories in one or more
-;; todo files, which are filtered by various criteria.
-
-;; To get started, load this package and type `M-x todo-show'.  This
-;; will prompt you for the name of the first todo file, its first
-;; category and the category's first item, create these and display
-;; them in Todo mode.  Now you can insert further items into the list
-;; (i.e., the category) and assign them priorities by typing `i i'.
-
-;; You will probably find it convenient to give `todo-show' a global
-;; key binding in your init file, since it is one of the entry points
-;; to Todo mode; a good choice is `C-c t', since `todo-show' is
-;; bound to `t' in Todo mode.
-
-;; To see a list of all Todo mode commands and their key bindings,
-;; including other entry points, type `C-h m' in Todo mode.  Consult
-;; the documentation strings of the commands for details of their use.
-;; The `todo' customization group and its subgroups list the options
-;; you can set to alter the behavior of many commands and various
-;; aspects of the display.
-
-;; This package is a new version of Oliver Seidel's todo-mode.el.
-;; While it retains the same basic organization and handling of todo
-;; lists and the basic UI, it significantly extends these and adds
-;; many features.  This required also making changes to the internals,
-;; including the file format.  If you have a todo file in old format,
-;; then the first time you invoke `todo-show' (i.e., before you have
-;; created any todo file in the current format), it will ask you
-;; whether to convert that file and show it.  If you choose not to
-;; convert the old-style file at this time, you can do so later by
-;; calling the command `todo-convert-legacy-files'.
+;; can add and edit todo items, reprioritize them, move them to
+;; another category, or delete them.  You can also mark items as done
+;; and store them within their category or in separate archive files.
+;; You can include todo items in the Emacs Fancy Diary display and
+;; treat them as appointments.  You can add new todo files, and rename
+;; or delete them.  You can add new categories to a file, rename or
+;; delete them, move a category to another file and merge the items of
+;; two categories.  You can also reorder the sequence of categories in
+;; a todo file for the purpose of navigation.  You can display
+;; sortable summary tables of the categories in a file and the types
+;; of items they contain.  And you can filter items by various
+;; criteria from multiple categories in one or more todo files to
+;; create prioritizable cross-category overviews of your todo items.
+
+;; To get started, type `M-x todo-show'.  For full details of the user
+;; interface, commands and options, consult the Todo mode user manual,
+;; which is included in the Info documentation.
 
 ;;; Code:
 
 (require 'diary-lib)
-;; For cl-remove-duplicates (in todo-insertion-commands-args) and
-;; cl-oddp.
-(require 'cl-lib)
+(require 'cl-lib)                      ; For cl-oddp and cl-assert.
 
 ;; -----------------------------------------------------------------------------
 ;;; Setting up todo files, categories, and items
@@ -100,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))))))
@@ -263,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
@@ -698,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
@@ -758,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))
@@ -769,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
@@ -1129,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))
@@ -1360,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")
@@ -1712,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)))
@@ -1736,31 +1721,40 @@ means prompt user and omit comment only on confirmation."
 
 (defun todo-toggle-mark-item (&optional n)
   "Mark item with `todo-item-mark' if unmarked, otherwise unmark it.
-With a positive numerical prefix argument N, change the
-marking of the next N items."
+With positive numerical prefix argument N, change the marking of
+the next N items in the current category.  If both the todo and
+done items sections are visible, the sequence of N items can
+consist of the the last todo items and the first done items."
   (interactive "p")
   (when (todo-item-string)
     (unless (> n 1) (setq n 1))
-    (dotimes (i n)
-      (let* ((cat (todo-current-category))
-            (marks (assoc cat todo-categories-with-marks))
-            (ov (progn
-                  (unless (looking-at todo-item-start)
-                    (todo-item-start))
-                  (todo-get-overlay 'prefix)))
-            (pref (overlay-get ov 'before-string)))
-       (if (todo-marked-item-p)
-           (progn
-             (overlay-put ov 'before-string (substring pref 1))
-             (if (= (cdr marks) 1)     ; Deleted last mark in this category.
-                 (setq todo-categories-with-marks
-                       (assq-delete-all cat todo-categories-with-marks))
-               (setcdr marks (1- (cdr marks)))))
-         (overlay-put ov 'before-string (concat todo-item-mark pref))
-         (if marks
-             (setcdr marks (1+ (cdr marks)))
-           (push (cons cat 1) todo-categories-with-marks))))
-      (todo-forward-item))))
+    (catch 'end
+      (dotimes (i n)
+       (let* ((cat (todo-current-category))
+              (marks (assoc cat todo-categories-with-marks))
+              (ov (progn
+                    (unless (looking-at todo-item-start)
+                      (todo-item-start))
+                    (todo-get-overlay 'prefix)))
+              (pref (overlay-get ov 'before-string)))
+         (if (todo-marked-item-p)
+             (progn
+               (overlay-put ov 'before-string (substring pref 1))
+               (if (= (cdr marks) 1)   ; Deleted last mark in this category.
+                   (setq todo-categories-with-marks
+                         (assq-delete-all cat todo-categories-with-marks))
+                 (setcdr marks (1- (cdr marks)))))
+           (overlay-put ov 'before-string (concat todo-item-mark pref))
+           (if marks
+               (setcdr marks (1+ (cdr marks)))
+             (push (cons cat 1) todo-categories-with-marks))))
+       (todo-forward-item)
+       ;; Don't try to mark the empty lines at the end of the todo
+       ;; and done items sections.
+       (when (looking-at "^$")
+         (if (eobp)
+             (throw 'end nil)
+           (todo-forward-item)))))))
 
 (defun todo-mark-category ()
   "Mark all visible items in this category with `todo-item-mark'."
@@ -1777,7 +1771,12 @@ marking of the next N items."
            (if marks
                (setcdr marks (1+ (cdr marks)))
              (push (cons cat 1) todo-categories-with-marks))))
-       (todo-forward-item)))))
+       (todo-forward-item)
+       ;; Don't try to mark the empty line between the todo and done
+       ;; items sections.
+       (when (looking-at "^$")
+         (unless (eobp)
+           (todo-forward-item)))))))
 
 (defun todo-unmark-category ()
   "Remove `todo-item-mark' from all visible items in this category."
@@ -2080,87 +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.
@@ -2215,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
@@ -2249,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)
@@ -2317,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"
@@ -2334,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 "*"))
@@ -2386,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.
@@ -2606,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.
@@ -2797,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.
@@ -2830,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 "]")))
@@ -2868,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 ()
@@ -2899,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))
@@ -3960,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")
@@ -3973,7 +3974,8 @@ regexp items."
     (setq file (cdr (assoc-string file falist)))
     (find-file file)
     (unless (derived-mode-p 'todo-filtered-items-mode)
-      (todo-filtered-items-mode))))
+      (todo-filtered-items-mode))
+    (todo-prefix-overlays)))
 
 (defun todo-go-to-source-item ()
   "Display the file and category of the filtered item at point."
@@ -4082,7 +4084,6 @@ multifile commands for further details."
                        (progn (todo-multiple-filter-files)
                               todo-multiple-filter-files))
                  (list todo-current-todo-file)))
-        (multi (> (length flist) 1))
         (fname (if (equal flist 'quit)
                    ;; Pressed `cancel' in t-m-f-f file selection dialog.
                    (keyboard-quit)
@@ -4091,6 +4092,7 @@ multifile commands for further details."
                          (cond (top ".todt")
                                (diary ".tody")
                                (regexp ".todr")))))
+        (multi (> (length flist) 1))
         (rxfiles (when regexp
                    (directory-files todo-directory t ".*\\.todr$" t)))
         (file-exists (or (file-exists-p fname) rxfiles))
@@ -4239,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))))))
@@ -4296,24 +4299,25 @@ set the user customizable option `todo-top-priorities-overrides'."
         (frule (assoc-string file rules))
         (crules (nth 2 frule))
         (crule (assoc-string cat crules))
-        (cur (or (and arg (cdr crule))
-                 (nth 1 frule)
-                 todo-top-priorities))
+        (fcur (or (nth 1 frule)
+                  todo-top-priorities))
+        (ccur (or (and arg (cdr crule))
+                  fcur))
         (prompt (if arg (concat "Number of top priorities in this category"
                                 " (currently %d): ")
                   (concat "Default number of top priorities per category"
                                 " in this file (currently %d): ")))
         (new -1))
     (while (< new 0)
-      (let ((cur0 cur))
-       (setq new (read-number (format prompt cur0))
+      (let ((cur (if arg ccur fcur)))
+       (setq new (read-number (format prompt cur))
              prompt "Enter a non-negative number: "
-             cur0 nil)))
+             cur nil)))
     (let ((nrule (if arg
                     (append (delete crule crules) (list (cons cat new)))
                   (append (list file new) (list crules)))))
       (setq rules (cons (if arg
-                           (list file cur nrule)
+                           (list file fcur nrule)
                          nrule)
                        (delete frule rules)))
       (customize-save-variable 'todo-top-priorities-overrides rules)
@@ -4648,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 "]"))
@@ -4886,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)))
@@ -5031,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 "
@@ -5204,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)))))
 
@@ -5215,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))
@@ -5234,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
@@ -5321,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
@@ -5419,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
@@ -5542,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))
@@ -5607,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))
@@ -5893,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
@@ -5986,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)
@@ -6039,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
@@ -6068,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)
@@ -6094,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)
@@ -6531,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
@@ -6541,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
@@ -6564,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)