Subject: bug#78608: 31.0.50; Todo mode bug revealed by setq-default change

From: Stephen Berman <[email-masked]>
Date: Tue, 27 May 2025 17:53:24 +0200

TL;DR: There is a bug in the command `todo-jump-to-category' from
todo-mode.el that manifests itself differently in master and in earlier
versions of Emacs due to the change to `setq-default' made in commit
f3e0bdf1b98.  A patch that fixes both manifestations is provided.

Background: As documented, `todo-jump-to-category' visits a
user-specified category of a given Todo file, and if the specified
category does not exist in the Todo file, the user is prompted whether
to add the category to the file, and if confirmed, then by default there
is a further prompt to add the first Todo item to the category.  The
command can also be invoked from outside of a todo-mode buffer, but
since it is not autoloaded, todo-mode must be loaded before it can be so
invoked.

Problem (master): Recently, I started observing aberrant behavior when
invoking `todo-jump-to-category' before visiting any Todo file in
todo-mode (I load todo-mode in my init file): upon jumping to an
existing category, I get prompted to add a Todo item, although the
category is not new.  To reproduce this:

0. HOME=/tmp/${USER} emacs -Q  (start Emacs in a clean environment)
1. Type `M-x todo-show' and at the prompt type "test RET test1 RET item1
   RET" to initialize the Todo file "test" with the category "test1"
   containing the item "item1".  The current buffer displays that
   category in todo-mode.
2. (Sanity check) Type `q' to save the newly created Todo file and bury
   the buffer, so that the current buffer is no longer a todo-mode
   buffer.  Then type `M-x todo-jump-to-category': this switches the
   buffer back to the todo-mode buffer displaying the category "test1"
   with the item "item1"; there is no prompt for a Todo item.
3. Kill the Emacs session and restart as in step 0.
4. Type `M-x load-library' and at the prompt enter `todo-mode RET'.
5. Type `M-x todo-jump-to-category' and at the prompt enter "test1" (or
   type TAB, which automatically inserts "test1" after the prompt, since
   it's the only category in the Todo file).
=> The category "test1" is displayed but now the minibuffer displays the
   prompt "Todo item: ".

Solution: The following patch fixes this, so that at step 5 the category
is displayed and there is no prompt to add an item.

diff --git a/lisp/calendar/todo-mode.el b/lisp/calendar/todo-mode.el
index 22be471ef82..ab5e61a0787 100644
--- a/lisp/calendar/todo-mode.el
+++ b/lisp/calendar/todo-mode.el
@@ -952,12 +952,9 @@ todo-jump-to-category
 				 todo-current-todo-file) ".toda")
 		      ;; Otherwise, jump to the category in the todo file.
 		      todo-current-todo-file)))
-	   (len (length todo-categories))
 	   (cat+file (unless cat
 		       (todo-read-category "Jump to category: "
 					    (if archive 'archive) file)))
-	   (add-item (and todo-add-item-if-new-category
-			  (> (length todo-categories) len)))
 	   (category (or cat (car cat+file))))
       (unless cat (setq file0 (cdr cat+file)))
       (with-current-buffer (find-file-noselect file0 'nowarn)
@@ -971,7 +968,10 @@ todo-jump-to-category
         (todo-category-select)
         (goto-char (point-min))
 	(if (bound-and-true-p hl-line-mode) (hl-line-highlight))
-        (when add-item (todo-insert-item--basic))))))
+        (when (and todo-add-item-if-new-category
+                   ;; A new category is empty on creation.
+                   (seq-every-p #'zerop (cdr (assoc category todo-categories))))
+          (todo-insert-item--basic))))))
 
 (defun todo-next-item (&optional count)
   "Move point down to the beginning of the next item.

Analysis: The current code in `todo-jump-to-category' compares, via the
buffer-local variable `todo-categories', the length of the list of Todo
categories in the specified Todo file before and after specifying the
category to jump to.  When `todo-jump-to-category' is invoked outside of
todo-mode, `todo-categories' is always nil, so its length is zero, but
when the user specifies the category to jump to, `todo-categories' gets
(via `todo-read-category' and functions called from it) a non-nil list
value, so the post-specification length of `todo-categories' is greater
than the pre-specification length, and together with the non-nil default
value of `todo-add-item-if-new-category', this results in the prompt for
a Todo item.

The above patch eliminates the before and after comparison involving
`todo-categories' and replaces it by a check of whether there are any
items in the category, since an existing category should always have at
least one item (perhaps done or archived).

Trigger: After I debugged the problem on master and came up with the
above patch, I wondered why I had never encountered the problem until
recently, because most of the code involved hasn't changed since the
current version of Todo mode was added to the Emacs repo twelve years
ago.  So I executed the above recipe in the emacs-30 branch and was
surprised to find that the bug does not happen there: after step 5, the
category "test1" is displayed and there is no prompt for a Todo item.
Debugging showed that this is because the value of `todo-categories' is
nil in emacs-30, both before and after specifying a category to jump to.
This difference between emacs-30 and master manifests itself in the
function `todo-category-completions' (called from `todo-read-category')
around the following code:

(with-current-buffer (find-file-noselect f 'nowarn)
  [...]
  (todo-mode)
  [...])

In both master and emacs-30, before entering this sexp, the buffer-local
value of `todo-categories' is nil, but after invoking `todo-mode' its
value (for the buffer visiting the Todo file `f') becomes non-nil.
After leaving the `with-current-buffer' form, `todo-categories' retains
its non-nil value in master, but reverts to nil in emacs-30.  The mode
function `todo-mode' calls `todo-modes-set-3', which contains the form
`(setq-local todo-categories (todo-set-categories))'.  This led me to
commit f3e0bdf1b98, and indeed, when I undo that change in master, the
problematic prompt on calling `todo-jump-to-category' does not happen.

Problem (pre-master): Then I tested invoking `todo-jump-to-category'
from outside of a todo-mode buffer and specifying a non-existing
category, and saw the same pattern, which, however, now reverses the
problem status: the new (empty) category is displayed and there is the
now expected prompt for a Todo item in master but, now wrongly, not in
emacs-30; thus, in this case the behavior in emacs-30 (and earlier)
contradicts the documentation.  To reproduce, carry out the above recipe
(in master and in emacs-30), but in step 5 enter "test2" at the prompt
instead of "test1".  (When I implemented `todo-jump-to-category', I
apparently failed to test this case and haven't made use of it since.)

Applying the above patch to emacs-30 also fixes this problem, and with
the patch the other case (specifying an existing category) still works
correctly in emacs-30.  The fact that the latter case also works
correctly in emacs-30 without the patch is evidently due to the
problematic behavior of `setq-local' fixed by f3e0bdf1b98.  The patch
has a narrow scope, so I think it's safe enough to commit to emacs-30,
even though the bug there is not a regression; but if that's not
acceptable, I'll commit it to master.

In GNU Emacs 31.0.50 (build 1, x86_64-pc-linux-gnu, GTK+ Version
 3.24.49, cairo version 1.18.4) of 2025-05-24 built on strobelfs2
Repository revision: 492adf20b91520e96fb198e6e936e3145359c43b
Repository branch: master
Windowing system distributor 'The X.Org Foundation', version 11.0.12101016
System Description: Linux From Scratch r12.3-20

Configured using:
 'configure -C 'CFLAGS=-Og -g3' PKG_CONFIG_PATH=/opt/qt6/lib/pkgconfig'

Configured features:
ACL CAIRO DBUS FREETYPE GIF GLIB GMP GNUTLS GPM GSETTINGS HARFBUZZ JPEG
LCMS2 LIBSYSTEMD LIBXML2 MODULES NATIVE_COMP NOTIFY INOTIFY PDUMPER PNG
RSVG SECCOMP SOUND SQLITE3 THREADS TIFF TOOLKIT_SCROLL_BARS TREE_SITTER
WEBP X11 XDBE XIM XINERAMA XINPUT2 XPM XRANDR GTK3 ZLIB