Subject: Re: bug#69941: 30.0.50; Faulty fontification of radio button widgets

From: Mauro Aranda <[email-masked]>
Date: Mon, 13 Jan 2025 11:35:54 -0300

It turns out the problem is general to all composite widgets.  Luckily,
it seems that it only surfaces in corner cases, and mostly as a
fontification problem.

Stefan detected the problem: widgets whose children get recreated (but
they themselves don't) might end up with their markers not containing
the entire child anymore.  But since it might happen with any composite
widget, we can't just fix this for radio or checklist widgets.

I attach a patch with a first idea (based on Stefan's suggestions),
where we "protect" the parent's markers, inside widget-default-create.
This is so we can try to catch almost all situations where this bug
might arise.  It doesn't catch _all_, in theory, since it might happen
that a grandparent widget doesn't adjust its markers, but maybe that's
unusual enough that we can get away with it.

The patch includes a test, that hopefully shows how easy is to make this
bug appear.
From 153828dadb5e61a7b446b2bc4c5573aac0caebaf Mon Sep 17 00:00:00 2001
From: Mauro Aranda <[email-masked]>
Date: Sun, 12 Jan 2025 17:28:41 -0300
Subject: [PATCH] Protect parent markers when recreating a child widget

Recreating child widgets without recreating the parent widget
may lead to situations where the parent widget doesn't correctly
cover its children or buttons anymore.  This bug manifests as a
faulty fontification of children or buttons, for example.
(Bug#69941)

* lisp/wid-edit.el (widget--protect-parent-markers)
(widget--unprotect-parent-markers): New functions.
(widget-default-create): Use them.
* test/lisp/wid-edit-tests.el (widget-test-parent-markers): New
test.
---
 lisp/wid-edit.el            | 32 ++++++++++++++++++++++++++++++++
 test/lisp/wid-edit-tests.el | 22 ++++++++++++++++++++++
 2 files changed, 54 insertions(+)

diff --git a/lisp/wid-edit.el b/lisp/wid-edit.el
index ca4e1e184cb..5c98cad00c6 100644
--- a/lisp/wid-edit.el
+++ b/lisp/wid-edit.el
@@ -880,6 +880,36 @@ widget-button-suffix
 
 ;;; Creating Widgets.
 
+(defun widget--protect-parent-markers (widget)
+  "Protect the :from and :to markers of the WIDGET's parent, if necessary.
+
+Usually, the :from marker has type t, while the :to marker has type nil.
+When creating a child or a button inside a composite widget, they have to be
+changed to nil and t respectively, so that the WIDGET's parent (if any),
+properly contains all of its children and buttons."
+  (let* ((parent (widget-get widget :parent))
+         (parent-from-marker (and parent (widget-get parent :from)))
+         (parent-to-marker (and parent (widget-get parent :to)))
+         (pos (point)))
+    (when (and parent-from-marker
+               (eq pos (marker-position parent-from-marker)))
+      (set-marker-insertion-type parent-from-marker nil))
+    (when (and parent-to-marker
+               (eq pos (marker-position parent-to-marker)))
+      (set-marker-insertion-type parent-to-marker t))))
+
+(defun widget--unprotect-parent-markers (widget)
+  "Unprotect the :from and :to markers of the WIDGET's parent, if necessary.
+
+Changes the :from marker type back to t and the :to marker type back to nil."
+  (let* ((parent (widget-get widget :parent))
+         (parent-from-marker (and parent (widget-get parent :from)))
+         (parent-to-marker (and parent (widget-get parent :to))))
+    (when parent-from-marker
+      (set-marker-insertion-type parent-from-marker t))
+    (when parent-to-marker
+      (set-marker-insertion-type parent-to-marker nil))))
+
 ;;;###autoload
 (defun widget-create (type &rest args)
   "Create widget of TYPE.
@@ -1735,6 +1765,7 @@ widget-default-create
 	 sample-begin sample-end
 	 doc-begin doc-end
 	 value-pos)
+     (widget--protect-parent-markers widget)
      (insert (widget-get widget :format))
      (goto-char from)
      ;; Parse escapes in format.
@@ -1795,6 +1826,7 @@ widget-default-create
      (when value-pos
        (goto-char value-pos)
        (widget-apply widget :value-create)))
+   (widget--unprotect-parent-markers widget)
    (let ((from (point-min-marker))
 	 (to (point-max-marker)))
      (set-marker-insertion-type from t)
diff --git a/test/lisp/wid-edit-tests.el b/test/lisp/wid-edit-tests.el
index 6f3c87a809f..791292384f7 100644
--- a/test/lisp/wid-edit-tests.el
+++ b/test/lisp/wid-edit-tests.el
@@ -414,4 +414,26 @@ widget-test-restricted-sexp-empty-val
       (delete-char 1)
       (should (string= (widget-value w) "")))))
 
+;; Bug#69941
+(ert-deftest widget-test-parent-markers ()
+  "Test that recreating a child keeps the parent's marker meaningful."
+  (with-temp-buffer
+    (let* ((group (widget-create 'group
+                                 :format "%v"
+                                 '(item :value 1 :format "%v")))
+           (item (car (widget-get group :children)))
+           (ofrom (widget-get group :from))
+           (oto (widget-get group :to)))
+      (widget-insert "\n")
+      (widget-setup)
+      ;; Change item, without recreating the group.  This causes changes
+      ;; right at the :from and :to markers, and if they don't have
+      ;; the right type, the group :from and :to span won't include its
+      ;; children, the item widget.
+      (widget-value-set item 2)
+      ;; The positions should be the same as they were when the group
+      ;; widget was first created.
+      (should (= ofrom (widget-get group :from)))
+      (should (= oto (widget-get group :to))))))
+
 ;;; wid-edit-tests.el ends here
-- 
2.34.1